HTRACE-108. Search Page: Use server side pagination
diff --git a/htrace-core/src/web/app/models/span.js b/htrace-core/src/web/app/models/span.js
index 85c7c72..1231101 100644
--- a/htrace-core/src/web/app/models/span.js
+++ b/htrace-core/src/web/app/models/span.js
@@ -55,25 +55,81 @@
 
 App.Spans = Backbone.PageableCollection.extend({
   model: App.Span,
-  mode: "client",
-  state: {
-    pageSize: 2
-  },
+  mode: "infinite",
   url: "/query",
+  state: {
+    pageSize: 10,
+    lastSpanId: null,
+    predicates: []
+  },
+  queryParams: {
+    totalPages: null,
+    totalRecords: null,
+    firstPage: null,
+    lastPage: null,
+    currentPage: null,
+    pageSize: null,
+    sortKey: null,
+    order: null,
+    directions: null,
 
-  query: function(options, predicates) {
-    var query = {
-      "lim": 100000
-    };
+    /**
+     * Query parameter for htraced.
+     */
+    query: function() {
+      var predicates = this.state.predicates.slice(0);
+      var lastSpanId = this.state.lastSpanId;
 
-    if (predicates && predicates.length > 0) {
-      query.pred = predicates;
+      /**
+       * Use last pulled span ID to paginate.
+       * The htraced API works such that order is defined by the first predicate.
+       * Adding a predicate to the end of the predicates list won't change the order.
+       * Providing the predicate on spanid will filter all previous spanids.
+       */
+      if (lastSpanId) {
+        predicates.push({
+          "op": "gt",
+          "field": "spanid",
+          "val": lastSpanId
+        });
+      }
+
+      return JSON.stringify({
+        lim: this.state.pageSize,
+        pred: predicates
+      });
+    }
+  },
+
+  initialize: function() {
+    this.on("sync", function(collection, response, options) {
+      if (response.length == 0) {
+        delete this.links[this.state.currentPage];
+        this.getPreviousPage();
+      }
+    }, this);
+  },
+
+  parseLinks: function(resp, xhr) {
+    if (resp.length >= this.state.pageSize) {
+      this.state.lastSpanId = resp[resp.length - 1].s;
+
+      return {
+        "next": "/query?query=" + this.queryParams.query.call(this)
+      };
+    } else {
+      this.state.lastSpanId = null;
+
+      return {};
+    }
+  },
+
+  setPredicates: function(predicates) {
+    if (!$.isArray(predicates)) {
+      console.error("predicates should be an array");
+      return;
     }
 
-    options = options ? _.clone(options) : {};
-    options.data = options.data ? _.clone(options.data) : {};
-    options.data.query = JSON.stringify(query);
-
-    return this.fetch(options);
+    this.state.predicates = predicates;
   }
 });
diff --git a/htrace-core/src/web/app/setup.js b/htrace-core/src/web/app/setup.js
index 9b78ba3..bb41ad8 100644
--- a/htrace-core/src/web/app/setup.js
+++ b/htrace-core/src/web/app/setup.js
@@ -26,7 +26,7 @@
 
   initialize: function() {
     this.spansCollection = new App.Spans();
-    this.spansCollection.query();
+    this.spansCollection.fetch();
 
     this.spanViews = {};
 
diff --git a/htrace-core/src/web/app/views/search.js b/htrace-core/src/web/app/views/search.js
index 40b9309..1016de3 100644
--- a/htrace-core/src/web/app/views/search.js
+++ b/htrace-core/src/web/app/views/search.js
@@ -76,7 +76,13 @@
       });
     }
 
-    this.collection.query(null, predicates);
+    this.collection.switchMode("infinite", {
+      fetch: false,
+      resetState: true
+    });
+    this.collection.fullCollection.reset();
+    this.collection.setPredicates(predicates);
+    this.collection.fetch();
 
     return false;
   }
diff --git a/htrace-core/src/web/app/views/span.js b/htrace-core/src/web/app/views/span.js
index 764b797..cbb694d 100644
--- a/htrace-core/src/web/app/views/span.js
+++ b/htrace-core/src/web/app/views/span.js
@@ -76,7 +76,7 @@
     });
 
     this.listSpansPaginator = new Backgrid.Extension.Paginator({
-      collection: this.collection
+      collection: this.collection,
     });
   },
 
diff --git a/htrace-core/src/web/index.html b/htrace-core/src/web/index.html
index 7f0773c..24ccc65 100644
--- a/htrace-core/src/web/index.html
+++ b/htrace-core/src/web/index.html
@@ -161,11 +161,11 @@
 
     <script src="lib/js/jquery-2.1.3.min.js" type="text/javascript"></script>
     <script src="lib/bootstrap-3.3.1/js/bootstrap.min.js" type="text/javascript"></script>
-    <script src="lib/js/underscore-1.7.0.min.js" type="text/javascript"></script>
-    <script src="lib/js/backbone-1.1.2.min.js" type="text/javascript"></script>
-    <script src="lib/js/backbone.paginator-2.0.2.min.js" type="text/javascript"></script>
-    <script src="lib/js/backgrid-0.3.5.min.js" type="text/javascript"></script>
-    <script src="lib/js/backgrid-paginator.js" type="text/javascript"></script>
+    <script src="lib/js/underscore-1.7.0.js" type="text/javascript"></script>
+    <script src="lib/js/backbone-1.1.2.js" type="text/javascript"></script>
+    <script src="lib/js/backbone.paginator-2.0.2.js" type="text/javascript"></script>
+    <script src="lib/js/backgrid-0.3.5.js" type="text/javascript"></script>
+    <script src="lib/js/backgrid-paginator-0.3.5.js" type="text/javascript"></script>
     <script src="lib/js/moment-2.9.0.min.js" type="text/javascript"></script>
     <script src="lib/pickadate-3.5.2/picker.js" type="text/javascript"></script>
     <script src="lib/pickadate-3.5.2/picker.date.js" type="text/javascript"></script>
@@ -173,9 +173,6 @@
 
     <script src="app/app.js" type="text/javascript"></script>
     <script src="app/models/span.js" type="text/javascript"></script>
-    
-    <script src="app/mock.js" type="text/javascript"></script>
-
     <script src="app/views/span.js" type="text/javascript"></script>
     <script src="app/views/search.js" type="text/javascript"></script>
     <script src="app/setup.js" type="text/javascript"></script>
diff --git a/htrace-core/src/web/lib/js/backbone-1.1.2.js b/htrace-core/src/web/lib/js/backbone-1.1.2.js
new file mode 100644
index 0000000..5da4943
--- /dev/null
+++ b/htrace-core/src/web/lib/js/backbone-1.1.2.js
@@ -0,0 +1,1608 @@
+//     Backbone.js 1.1.2
+
+//     (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+//     Backbone may be freely distributed under the MIT license.
+//     For all details and documentation:
+//     http://backbonejs.org
+
+(function(root, factory) {
+
+  // Set up Backbone appropriately for the environment. Start with AMD.
+  if (typeof define === 'function' && define.amd) {
+    define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
+      // Export global even in AMD case in case this script is loaded with
+      // others that may still expect a global Backbone.
+      root.Backbone = factory(root, exports, _, $);
+    });
+
+  // Next for Node.js or CommonJS. jQuery may not be needed as a module.
+  } else if (typeof exports !== 'undefined') {
+    var _ = require('underscore');
+    factory(root, exports, _);
+
+  // Finally, as a browser global.
+  } else {
+    root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
+  }
+
+}(this, function(root, Backbone, _, $) {
+
+  // Initial Setup
+  // -------------
+
+  // Save the previous value of the `Backbone` variable, so that it can be
+  // restored later on, if `noConflict` is used.
+  var previousBackbone = root.Backbone;
+
+  // Create local references to array methods we'll want to use later.
+  var array = [];
+  var push = array.push;
+  var slice = array.slice;
+  var splice = array.splice;
+
+  // Current version of the library. Keep in sync with `package.json`.
+  Backbone.VERSION = '1.1.2';
+
+  // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
+  // the `$` variable.
+  Backbone.$ = $;
+
+  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
+  // to its previous owner. Returns a reference to this Backbone object.
+  Backbone.noConflict = function() {
+    root.Backbone = previousBackbone;
+    return this;
+  };
+
+  // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
+  // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
+  // set a `X-Http-Method-Override` header.
+  Backbone.emulateHTTP = false;
+
+  // Turn on `emulateJSON` to support legacy servers that can't deal with direct
+  // `application/json` requests ... will encode the body as
+  // `application/x-www-form-urlencoded` instead and will send the model in a
+  // form param named `model`.
+  Backbone.emulateJSON = false;
+
+  // Backbone.Events
+  // ---------------
+
+  // A module that can be mixed in to *any object* in order to provide it with
+  // custom events. You may bind with `on` or remove with `off` callback
+  // functions to an event; `trigger`-ing an event fires all callbacks in
+  // succession.
+  //
+  //     var object = {};
+  //     _.extend(object, Backbone.Events);
+  //     object.on('expand', function(){ alert('expanded'); });
+  //     object.trigger('expand');
+  //
+  var Events = Backbone.Events = {
+
+    // Bind an event to a `callback` function. Passing `"all"` will bind
+    // the callback to all events fired.
+    on: function(name, callback, context) {
+      if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
+      this._events || (this._events = {});
+      var events = this._events[name] || (this._events[name] = []);
+      events.push({callback: callback, context: context, ctx: context || this});
+      return this;
+    },
+
+    // Bind an event to only be triggered a single time. After the first time
+    // the callback is invoked, it will be removed.
+    once: function(name, callback, context) {
+      if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
+      var self = this;
+      var once = _.once(function() {
+        self.off(name, once);
+        callback.apply(this, arguments);
+      });
+      once._callback = callback;
+      return this.on(name, once, context);
+    },
+
+    // Remove one or many callbacks. If `context` is null, removes all
+    // callbacks with that function. If `callback` is null, removes all
+    // callbacks for the event. If `name` is null, removes all bound
+    // callbacks for all events.
+    off: function(name, callback, context) {
+      var retain, ev, events, names, i, l, j, k;
+      if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
+      if (!name && !callback && !context) {
+        this._events = void 0;
+        return this;
+      }
+      names = name ? [name] : _.keys(this._events);
+      for (i = 0, l = names.length; i < l; i++) {
+        name = names[i];
+        if (events = this._events[name]) {
+          this._events[name] = retain = [];
+          if (callback || context) {
+            for (j = 0, k = events.length; j < k; j++) {
+              ev = events[j];
+              if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
+                  (context && context !== ev.context)) {
+                retain.push(ev);
+              }
+            }
+          }
+          if (!retain.length) delete this._events[name];
+        }
+      }
+
+      return this;
+    },
+
+    // Trigger one or many events, firing all bound callbacks. Callbacks are
+    // passed the same arguments as `trigger` is, apart from the event name
+    // (unless you're listening on `"all"`, which will cause your callback to
+    // receive the true name of the event as the first argument).
+    trigger: function(name) {
+      if (!this._events) return this;
+      var args = slice.call(arguments, 1);
+      if (!eventsApi(this, 'trigger', name, args)) return this;
+      var events = this._events[name];
+      var allEvents = this._events.all;
+      if (events) triggerEvents(events, args);
+      if (allEvents) triggerEvents(allEvents, arguments);
+      return this;
+    },
+
+    // Tell this object to stop listening to either specific events ... or
+    // to every object it's currently listening to.
+    stopListening: function(obj, name, callback) {
+      var listeningTo = this._listeningTo;
+      if (!listeningTo) return this;
+      var remove = !name && !callback;
+      if (!callback && typeof name === 'object') callback = this;
+      if (obj) (listeningTo = {})[obj._listenId] = obj;
+      for (var id in listeningTo) {
+        obj = listeningTo[id];
+        obj.off(name, callback, this);
+        if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
+      }
+      return this;
+    }
+
+  };
+
+  // Regular expression used to split event strings.
+  var eventSplitter = /\s+/;
+
+  // Implement fancy features of the Events API such as multiple event
+  // names `"change blur"` and jQuery-style event maps `{change: action}`
+  // in terms of the existing API.
+  var eventsApi = function(obj, action, name, rest) {
+    if (!name) return true;
+
+    // Handle event maps.
+    if (typeof name === 'object') {
+      for (var key in name) {
+        obj[action].apply(obj, [key, name[key]].concat(rest));
+      }
+      return false;
+    }
+
+    // Handle space separated event names.
+    if (eventSplitter.test(name)) {
+      var names = name.split(eventSplitter);
+      for (var i = 0, l = names.length; i < l; i++) {
+        obj[action].apply(obj, [names[i]].concat(rest));
+      }
+      return false;
+    }
+
+    return true;
+  };
+
+  // A difficult-to-believe, but optimized internal dispatch function for
+  // triggering events. Tries to keep the usual cases speedy (most internal
+  // Backbone events have 3 arguments).
+  var triggerEvents = function(events, args) {
+    var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
+    switch (args.length) {
+      case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
+      case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
+      case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
+      case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
+      default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
+    }
+  };
+
+  var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
+
+  // Inversion-of-control versions of `on` and `once`. Tell *this* object to
+  // listen to an event in another object ... keeping track of what it's
+  // listening to.
+  _.each(listenMethods, function(implementation, method) {
+    Events[method] = function(obj, name, callback) {
+      var listeningTo = this._listeningTo || (this._listeningTo = {});
+      var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
+      listeningTo[id] = obj;
+      if (!callback && typeof name === 'object') callback = this;
+      obj[implementation](name, callback, this);
+      return this;
+    };
+  });
+
+  // Aliases for backwards compatibility.
+  Events.bind   = Events.on;
+  Events.unbind = Events.off;
+
+  // Allow the `Backbone` object to serve as a global event bus, for folks who
+  // want global "pubsub" in a convenient place.
+  _.extend(Backbone, Events);
+
+  // Backbone.Model
+  // --------------
+
+  // Backbone **Models** are the basic data object in the framework --
+  // frequently representing a row in a table in a database on your server.
+  // A discrete chunk of data and a bunch of useful, related methods for
+  // performing computations and transformations on that data.
+
+  // Create a new model with the specified attributes. A client id (`cid`)
+  // is automatically generated and assigned for you.
+  var Model = Backbone.Model = function(attributes, options) {
+    var attrs = attributes || {};
+    options || (options = {});
+    this.cid = _.uniqueId('c');
+    this.attributes = {};
+    if (options.collection) this.collection = options.collection;
+    if (options.parse) attrs = this.parse(attrs, options) || {};
+    attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
+    this.set(attrs, options);
+    this.changed = {};
+    this.initialize.apply(this, arguments);
+  };
+
+  // Attach all inheritable methods to the Model prototype.
+  _.extend(Model.prototype, Events, {
+
+    // A hash of attributes whose current and previous value differ.
+    changed: null,
+
+    // The value returned during the last failed validation.
+    validationError: null,
+
+    // The default name for the JSON `id` attribute is `"id"`. MongoDB and
+    // CouchDB users may want to set this to `"_id"`.
+    idAttribute: 'id',
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize: function(){},
+
+    // Return a copy of the model's `attributes` object.
+    toJSON: function(options) {
+      return _.clone(this.attributes);
+    },
+
+    // Proxy `Backbone.sync` by default -- but override this if you need
+    // custom syncing semantics for *this* particular model.
+    sync: function() {
+      return Backbone.sync.apply(this, arguments);
+    },
+
+    // Get the value of an attribute.
+    get: function(attr) {
+      return this.attributes[attr];
+    },
+
+    // Get the HTML-escaped value of an attribute.
+    escape: function(attr) {
+      return _.escape(this.get(attr));
+    },
+
+    // Returns `true` if the attribute contains a value that is not null
+    // or undefined.
+    has: function(attr) {
+      return this.get(attr) != null;
+    },
+
+    // Set a hash of model attributes on the object, firing `"change"`. This is
+    // the core primitive operation of a model, updating the data and notifying
+    // anyone who needs to know about the change in state. The heart of the beast.
+    set: function(key, val, options) {
+      var attr, attrs, unset, changes, silent, changing, prev, current;
+      if (key == null) return this;
+
+      // Handle both `"key", value` and `{key: value}` -style arguments.
+      if (typeof key === 'object') {
+        attrs = key;
+        options = val;
+      } else {
+        (attrs = {})[key] = val;
+      }
+
+      options || (options = {});
+
+      // Run validation.
+      if (!this._validate(attrs, options)) return false;
+
+      // Extract attributes and options.
+      unset           = options.unset;
+      silent          = options.silent;
+      changes         = [];
+      changing        = this._changing;
+      this._changing  = true;
+
+      if (!changing) {
+        this._previousAttributes = _.clone(this.attributes);
+        this.changed = {};
+      }
+      current = this.attributes, prev = this._previousAttributes;
+
+      // Check for changes of `id`.
+      if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
+
+      // For each `set` attribute, update or delete the current value.
+      for (attr in attrs) {
+        val = attrs[attr];
+        if (!_.isEqual(current[attr], val)) changes.push(attr);
+        if (!_.isEqual(prev[attr], val)) {
+          this.changed[attr] = val;
+        } else {
+          delete this.changed[attr];
+        }
+        unset ? delete current[attr] : current[attr] = val;
+      }
+
+      // Trigger all relevant attribute changes.
+      if (!silent) {
+        if (changes.length) this._pending = options;
+        for (var i = 0, l = changes.length; i < l; i++) {
+          this.trigger('change:' + changes[i], this, current[changes[i]], options);
+        }
+      }
+
+      // You might be wondering why there's a `while` loop here. Changes can
+      // be recursively nested within `"change"` events.
+      if (changing) return this;
+      if (!silent) {
+        while (this._pending) {
+          options = this._pending;
+          this._pending = false;
+          this.trigger('change', this, options);
+        }
+      }
+      this._pending = false;
+      this._changing = false;
+      return this;
+    },
+
+    // Remove an attribute from the model, firing `"change"`. `unset` is a noop
+    // if the attribute doesn't exist.
+    unset: function(attr, options) {
+      return this.set(attr, void 0, _.extend({}, options, {unset: true}));
+    },
+
+    // Clear all attributes on the model, firing `"change"`.
+    clear: function(options) {
+      var attrs = {};
+      for (var key in this.attributes) attrs[key] = void 0;
+      return this.set(attrs, _.extend({}, options, {unset: true}));
+    },
+
+    // Determine if the model has changed since the last `"change"` event.
+    // If you specify an attribute name, determine if that attribute has changed.
+    hasChanged: function(attr) {
+      if (attr == null) return !_.isEmpty(this.changed);
+      return _.has(this.changed, attr);
+    },
+
+    // Return an object containing all the attributes that have changed, or
+    // false if there are no changed attributes. Useful for determining what
+    // parts of a view need to be updated and/or what attributes need to be
+    // persisted to the server. Unset attributes will be set to undefined.
+    // You can also pass an attributes object to diff against the model,
+    // determining if there *would be* a change.
+    changedAttributes: function(diff) {
+      if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
+      var val, changed = false;
+      var old = this._changing ? this._previousAttributes : this.attributes;
+      for (var attr in diff) {
+        if (_.isEqual(old[attr], (val = diff[attr]))) continue;
+        (changed || (changed = {}))[attr] = val;
+      }
+      return changed;
+    },
+
+    // Get the previous value of an attribute, recorded at the time the last
+    // `"change"` event was fired.
+    previous: function(attr) {
+      if (attr == null || !this._previousAttributes) return null;
+      return this._previousAttributes[attr];
+    },
+
+    // Get all of the attributes of the model at the time of the previous
+    // `"change"` event.
+    previousAttributes: function() {
+      return _.clone(this._previousAttributes);
+    },
+
+    // Fetch the model from the server. If the server's representation of the
+    // model differs from its current attributes, they will be overridden,
+    // triggering a `"change"` event.
+    fetch: function(options) {
+      options = options ? _.clone(options) : {};
+      if (options.parse === void 0) options.parse = true;
+      var model = this;
+      var success = options.success;
+      options.success = function(resp) {
+        if (!model.set(model.parse(resp, options), options)) return false;
+        if (success) success(model, resp, options);
+        model.trigger('sync', model, resp, options);
+      };
+      wrapError(this, options);
+      return this.sync('read', this, options);
+    },
+
+    // Set a hash of model attributes, and sync the model to the server.
+    // If the server returns an attributes hash that differs, the model's
+    // state will be `set` again.
+    save: function(key, val, options) {
+      var attrs, method, xhr, attributes = this.attributes;
+
+      // Handle both `"key", value` and `{key: value}` -style arguments.
+      if (key == null || typeof key === 'object') {
+        attrs = key;
+        options = val;
+      } else {
+        (attrs = {})[key] = val;
+      }
+
+      options = _.extend({validate: true}, options);
+
+      // If we're not waiting and attributes exist, save acts as
+      // `set(attr).save(null, opts)` with validation. Otherwise, check if
+      // the model will be valid when the attributes, if any, are set.
+      if (attrs && !options.wait) {
+        if (!this.set(attrs, options)) return false;
+      } else {
+        if (!this._validate(attrs, options)) return false;
+      }
+
+      // Set temporary attributes if `{wait: true}`.
+      if (attrs && options.wait) {
+        this.attributes = _.extend({}, attributes, attrs);
+      }
+
+      // After a successful server-side save, the client is (optionally)
+      // updated with the server-side state.
+      if (options.parse === void 0) options.parse = true;
+      var model = this;
+      var success = options.success;
+      options.success = function(resp) {
+        // Ensure attributes are restored during synchronous saves.
+        model.attributes = attributes;
+        var serverAttrs = model.parse(resp, options);
+        if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
+        if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
+          return false;
+        }
+        if (success) success(model, resp, options);
+        model.trigger('sync', model, resp, options);
+      };
+      wrapError(this, options);
+
+      method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
+      if (method === 'patch') options.attrs = attrs;
+      xhr = this.sync(method, this, options);
+
+      // Restore attributes.
+      if (attrs && options.wait) this.attributes = attributes;
+
+      return xhr;
+    },
+
+    // Destroy this model on the server if it was already persisted.
+    // Optimistically removes the model from its collection, if it has one.
+    // If `wait: true` is passed, waits for the server to respond before removal.
+    destroy: function(options) {
+      options = options ? _.clone(options) : {};
+      var model = this;
+      var success = options.success;
+
+      var destroy = function() {
+        model.trigger('destroy', model, model.collection, options);
+      };
+
+      options.success = function(resp) {
+        if (options.wait || model.isNew()) destroy();
+        if (success) success(model, resp, options);
+        if (!model.isNew()) model.trigger('sync', model, resp, options);
+      };
+
+      if (this.isNew()) {
+        options.success();
+        return false;
+      }
+      wrapError(this, options);
+
+      var xhr = this.sync('delete', this, options);
+      if (!options.wait) destroy();
+      return xhr;
+    },
+
+    // Default URL for the model's representation on the server -- if you're
+    // using Backbone's restful methods, override this to change the endpoint
+    // that will be called.
+    url: function() {
+      var base =
+        _.result(this, 'urlRoot') ||
+        _.result(this.collection, 'url') ||
+        urlError();
+      if (this.isNew()) return base;
+      return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id);
+    },
+
+    // **parse** converts a response into the hash of attributes to be `set` on
+    // the model. The default implementation is just to pass the response along.
+    parse: function(resp, options) {
+      return resp;
+    },
+
+    // Create a new model with identical attributes to this one.
+    clone: function() {
+      return new this.constructor(this.attributes);
+    },
+
+    // A model is new if it has never been saved to the server, and lacks an id.
+    isNew: function() {
+      return !this.has(this.idAttribute);
+    },
+
+    // Check if the model is currently in a valid state.
+    isValid: function(options) {
+      return this._validate({}, _.extend(options || {}, { validate: true }));
+    },
+
+    // Run validation against the next complete set of model attributes,
+    // returning `true` if all is well. Otherwise, fire an `"invalid"` event.
+    _validate: function(attrs, options) {
+      if (!options.validate || !this.validate) return true;
+      attrs = _.extend({}, this.attributes, attrs);
+      var error = this.validationError = this.validate(attrs, options) || null;
+      if (!error) return true;
+      this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
+      return false;
+    }
+
+  });
+
+  // Underscore methods that we want to implement on the Model.
+  var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];
+
+  // Mix in each Underscore method as a proxy to `Model#attributes`.
+  _.each(modelMethods, function(method) {
+    Model.prototype[method] = function() {
+      var args = slice.call(arguments);
+      args.unshift(this.attributes);
+      return _[method].apply(_, args);
+    };
+  });
+
+  // Backbone.Collection
+  // -------------------
+
+  // If models tend to represent a single row of data, a Backbone Collection is
+  // more analagous to a table full of data ... or a small slice or page of that
+  // table, or a collection of rows that belong together for a particular reason
+  // -- all of the messages in this particular folder, all of the documents
+  // belonging to this particular author, and so on. Collections maintain
+  // indexes of their models, both in order, and for lookup by `id`.
+
+  // Create a new **Collection**, perhaps to contain a specific type of `model`.
+  // If a `comparator` is specified, the Collection will maintain
+  // its models in sort order, as they're added and removed.
+  var Collection = Backbone.Collection = function(models, options) {
+    options || (options = {});
+    if (options.model) this.model = options.model;
+    if (options.comparator !== void 0) this.comparator = options.comparator;
+    this._reset();
+    this.initialize.apply(this, arguments);
+    if (models) this.reset(models, _.extend({silent: true}, options));
+  };
+
+  // Default options for `Collection#set`.
+  var setOptions = {add: true, remove: true, merge: true};
+  var addOptions = {add: true, remove: false};
+
+  // Define the Collection's inheritable methods.
+  _.extend(Collection.prototype, Events, {
+
+    // The default model for a collection is just a **Backbone.Model**.
+    // This should be overridden in most cases.
+    model: Model,
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize: function(){},
+
+    // The JSON representation of a Collection is an array of the
+    // models' attributes.
+    toJSON: function(options) {
+      return this.map(function(model){ return model.toJSON(options); });
+    },
+
+    // Proxy `Backbone.sync` by default.
+    sync: function() {
+      return Backbone.sync.apply(this, arguments);
+    },
+
+    // Add a model, or list of models to the set.
+    add: function(models, options) {
+      return this.set(models, _.extend({merge: false}, options, addOptions));
+    },
+
+    // Remove a model, or a list of models from the set.
+    remove: function(models, options) {
+      var singular = !_.isArray(models);
+      models = singular ? [models] : _.clone(models);
+      options || (options = {});
+      var i, l, index, model;
+      for (i = 0, l = models.length; i < l; i++) {
+        model = models[i] = this.get(models[i]);
+        if (!model) continue;
+        delete this._byId[model.id];
+        delete this._byId[model.cid];
+        index = this.indexOf(model);
+        this.models.splice(index, 1);
+        this.length--;
+        if (!options.silent) {
+          options.index = index;
+          model.trigger('remove', model, this, options);
+        }
+        this._removeReference(model, options);
+      }
+      return singular ? models[0] : models;
+    },
+
+    // Update a collection by `set`-ing a new list of models, adding new ones,
+    // removing models that are no longer present, and merging models that
+    // already exist in the collection, as necessary. Similar to **Model#set**,
+    // the core operation for updating the data contained by the collection.
+    set: function(models, options) {
+      options = _.defaults({}, options, setOptions);
+      if (options.parse) models = this.parse(models, options);
+      var singular = !_.isArray(models);
+      models = singular ? (models ? [models] : []) : _.clone(models);
+      var i, l, id, model, attrs, existing, sort;
+      var at = options.at;
+      var targetModel = this.model;
+      var sortable = this.comparator && (at == null) && options.sort !== false;
+      var sortAttr = _.isString(this.comparator) ? this.comparator : null;
+      var toAdd = [], toRemove = [], modelMap = {};
+      var add = options.add, merge = options.merge, remove = options.remove;
+      var order = !sortable && add && remove ? [] : false;
+
+      // Turn bare objects into model references, and prevent invalid models
+      // from being added.
+      for (i = 0, l = models.length; i < l; i++) {
+        attrs = models[i] || {};
+        if (attrs instanceof Model) {
+          id = model = attrs;
+        } else {
+          id = attrs[targetModel.prototype.idAttribute || 'id'];
+        }
+
+        // If a duplicate is found, prevent it from being added and
+        // optionally merge it into the existing model.
+        if (existing = this.get(id)) {
+          if (remove) modelMap[existing.cid] = true;
+          if (merge) {
+            attrs = attrs === model ? model.attributes : attrs;
+            if (options.parse) attrs = existing.parse(attrs, options);
+            existing.set(attrs, options);
+            if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
+          }
+          models[i] = existing;
+
+        // If this is a new, valid model, push it to the `toAdd` list.
+        } else if (add) {
+          model = models[i] = this._prepareModel(attrs, options);
+          if (!model) continue;
+          toAdd.push(model);
+          this._addReference(model, options);
+        }
+
+        // Do not add multiple models with the same `id`.
+        model = existing || model;
+        if (order && (model.isNew() || !modelMap[model.id])) order.push(model);
+        modelMap[model.id] = true;
+      }
+
+      // Remove nonexistent models if appropriate.
+      if (remove) {
+        for (i = 0, l = this.length; i < l; ++i) {
+          if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
+        }
+        if (toRemove.length) this.remove(toRemove, options);
+      }
+
+      // See if sorting is needed, update `length` and splice in new models.
+      if (toAdd.length || (order && order.length)) {
+        if (sortable) sort = true;
+        this.length += toAdd.length;
+        if (at != null) {
+          for (i = 0, l = toAdd.length; i < l; i++) {
+            this.models.splice(at + i, 0, toAdd[i]);
+          }
+        } else {
+          if (order) this.models.length = 0;
+          var orderedModels = order || toAdd;
+          for (i = 0, l = orderedModels.length; i < l; i++) {
+            this.models.push(orderedModels[i]);
+          }
+        }
+      }
+
+      // Silently sort the collection if appropriate.
+      if (sort) this.sort({silent: true});
+
+      // Unless silenced, it's time to fire all appropriate add/sort events.
+      if (!options.silent) {
+        for (i = 0, l = toAdd.length; i < l; i++) {
+          (model = toAdd[i]).trigger('add', model, this, options);
+        }
+        if (sort || (order && order.length)) this.trigger('sort', this, options);
+      }
+
+      // Return the added (or merged) model (or models).
+      return singular ? models[0] : models;
+    },
+
+    // When you have more items than you want to add or remove individually,
+    // you can reset the entire set with a new list of models, without firing
+    // any granular `add` or `remove` events. Fires `reset` when finished.
+    // Useful for bulk operations and optimizations.
+    reset: function(models, options) {
+      options || (options = {});
+      for (var i = 0, l = this.models.length; i < l; i++) {
+        this._removeReference(this.models[i], options);
+      }
+      options.previousModels = this.models;
+      this._reset();
+      models = this.add(models, _.extend({silent: true}, options));
+      if (!options.silent) this.trigger('reset', this, options);
+      return models;
+    },
+
+    // Add a model to the end of the collection.
+    push: function(model, options) {
+      return this.add(model, _.extend({at: this.length}, options));
+    },
+
+    // Remove a model from the end of the collection.
+    pop: function(options) {
+      var model = this.at(this.length - 1);
+      this.remove(model, options);
+      return model;
+    },
+
+    // Add a model to the beginning of the collection.
+    unshift: function(model, options) {
+      return this.add(model, _.extend({at: 0}, options));
+    },
+
+    // Remove a model from the beginning of the collection.
+    shift: function(options) {
+      var model = this.at(0);
+      this.remove(model, options);
+      return model;
+    },
+
+    // Slice out a sub-array of models from the collection.
+    slice: function() {
+      return slice.apply(this.models, arguments);
+    },
+
+    // Get a model from the set by id.
+    get: function(obj) {
+      if (obj == null) return void 0;
+      return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid];
+    },
+
+    // Get the model at the given index.
+    at: function(index) {
+      return this.models[index];
+    },
+
+    // Return models with matching attributes. Useful for simple cases of
+    // `filter`.
+    where: function(attrs, first) {
+      if (_.isEmpty(attrs)) return first ? void 0 : [];
+      return this[first ? 'find' : 'filter'](function(model) {
+        for (var key in attrs) {
+          if (attrs[key] !== model.get(key)) return false;
+        }
+        return true;
+      });
+    },
+
+    // Return the first model with matching attributes. Useful for simple cases
+    // of `find`.
+    findWhere: function(attrs) {
+      return this.where(attrs, true);
+    },
+
+    // Force the collection to re-sort itself. You don't need to call this under
+    // normal circumstances, as the set will maintain sort order as each item
+    // is added.
+    sort: function(options) {
+      if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
+      options || (options = {});
+
+      // Run sort based on type of `comparator`.
+      if (_.isString(this.comparator) || this.comparator.length === 1) {
+        this.models = this.sortBy(this.comparator, this);
+      } else {
+        this.models.sort(_.bind(this.comparator, this));
+      }
+
+      if (!options.silent) this.trigger('sort', this, options);
+      return this;
+    },
+
+    // Pluck an attribute from each model in the collection.
+    pluck: function(attr) {
+      return _.invoke(this.models, 'get', attr);
+    },
+
+    // Fetch the default set of models for this collection, resetting the
+    // collection when they arrive. If `reset: true` is passed, the response
+    // data will be passed through the `reset` method instead of `set`.
+    fetch: function(options) {
+      options = options ? _.clone(options) : {};
+      if (options.parse === void 0) options.parse = true;
+      var success = options.success;
+      var collection = this;
+      options.success = function(resp) {
+        var method = options.reset ? 'reset' : 'set';
+        collection[method](resp, options);
+        if (success) success(collection, resp, options);
+        collection.trigger('sync', collection, resp, options);
+      };
+      wrapError(this, options);
+      return this.sync('read', this, options);
+    },
+
+    // Create a new instance of a model in this collection. Add the model to the
+    // collection immediately, unless `wait: true` is passed, in which case we
+    // wait for the server to agree.
+    create: function(model, options) {
+      options = options ? _.clone(options) : {};
+      if (!(model = this._prepareModel(model, options))) return false;
+      if (!options.wait) this.add(model, options);
+      var collection = this;
+      var success = options.success;
+      options.success = function(model, resp) {
+        if (options.wait) collection.add(model, options);
+        if (success) success(model, resp, options);
+      };
+      model.save(null, options);
+      return model;
+    },
+
+    // **parse** converts a response into a list of models to be added to the
+    // collection. The default implementation is just to pass it through.
+    parse: function(resp, options) {
+      return resp;
+    },
+
+    // Create a new collection with an identical list of models as this one.
+    clone: function() {
+      return new this.constructor(this.models);
+    },
+
+    // Private method to reset all internal state. Called when the collection
+    // is first initialized or reset.
+    _reset: function() {
+      this.length = 0;
+      this.models = [];
+      this._byId  = {};
+    },
+
+    // Prepare a hash of attributes (or other model) to be added to this
+    // collection.
+    _prepareModel: function(attrs, options) {
+      if (attrs instanceof Model) return attrs;
+      options = options ? _.clone(options) : {};
+      options.collection = this;
+      var model = new this.model(attrs, options);
+      if (!model.validationError) return model;
+      this.trigger('invalid', this, model.validationError, options);
+      return false;
+    },
+
+    // Internal method to create a model's ties to a collection.
+    _addReference: function(model, options) {
+      this._byId[model.cid] = model;
+      if (model.id != null) this._byId[model.id] = model;
+      if (!model.collection) model.collection = this;
+      model.on('all', this._onModelEvent, this);
+    },
+
+    // Internal method to sever a model's ties to a collection.
+    _removeReference: function(model, options) {
+      if (this === model.collection) delete model.collection;
+      model.off('all', this._onModelEvent, this);
+    },
+
+    // Internal method called every time a model in the set fires an event.
+    // Sets need to update their indexes when models change ids. All other
+    // events simply proxy through. "add" and "remove" events that originate
+    // in other collections are ignored.
+    _onModelEvent: function(event, model, collection, options) {
+      if ((event === 'add' || event === 'remove') && collection !== this) return;
+      if (event === 'destroy') this.remove(model, options);
+      if (model && event === 'change:' + model.idAttribute) {
+        delete this._byId[model.previous(model.idAttribute)];
+        if (model.id != null) this._byId[model.id] = model;
+      }
+      this.trigger.apply(this, arguments);
+    }
+
+  });
+
+  // Underscore methods that we want to implement on the Collection.
+  // 90% of the core usefulness of Backbone Collections is actually implemented
+  // right here:
+  var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
+    'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
+    'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
+    'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
+    'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle',
+    'lastIndexOf', 'isEmpty', 'chain', 'sample'];
+
+  // Mix in each Underscore method as a proxy to `Collection#models`.
+  _.each(methods, function(method) {
+    Collection.prototype[method] = function() {
+      var args = slice.call(arguments);
+      args.unshift(this.models);
+      return _[method].apply(_, args);
+    };
+  });
+
+  // Underscore methods that take a property name as an argument.
+  var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
+
+  // Use attributes instead of properties.
+  _.each(attributeMethods, function(method) {
+    Collection.prototype[method] = function(value, context) {
+      var iterator = _.isFunction(value) ? value : function(model) {
+        return model.get(value);
+      };
+      return _[method](this.models, iterator, context);
+    };
+  });
+
+  // Backbone.View
+  // -------------
+
+  // Backbone Views are almost more convention than they are actual code. A View
+  // is simply a JavaScript object that represents a logical chunk of UI in the
+  // DOM. This might be a single item, an entire list, a sidebar or panel, or
+  // even the surrounding frame which wraps your whole app. Defining a chunk of
+  // UI as a **View** allows you to define your DOM events declaratively, without
+  // having to worry about render order ... and makes it easy for the view to
+  // react to specific changes in the state of your models.
+
+  // Creating a Backbone.View creates its initial element outside of the DOM,
+  // if an existing element is not provided...
+  var View = Backbone.View = function(options) {
+    this.cid = _.uniqueId('view');
+    options || (options = {});
+    _.extend(this, _.pick(options, viewOptions));
+    this._ensureElement();
+    this.initialize.apply(this, arguments);
+    this.delegateEvents();
+  };
+
+  // Cached regex to split keys for `delegate`.
+  var delegateEventSplitter = /^(\S+)\s*(.*)$/;
+
+  // List of view options to be merged as properties.
+  var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
+
+  // Set up all inheritable **Backbone.View** properties and methods.
+  _.extend(View.prototype, Events, {
+
+    // The default `tagName` of a View's element is `"div"`.
+    tagName: 'div',
+
+    // jQuery delegate for element lookup, scoped to DOM elements within the
+    // current view. This should be preferred to global lookups where possible.
+    $: function(selector) {
+      return this.$el.find(selector);
+    },
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize: function(){},
+
+    // **render** is the core function that your view should override, in order
+    // to populate its element (`this.el`), with the appropriate HTML. The
+    // convention is for **render** to always return `this`.
+    render: function() {
+      return this;
+    },
+
+    // Remove this view by taking the element out of the DOM, and removing any
+    // applicable Backbone.Events listeners.
+    remove: function() {
+      this.$el.remove();
+      this.stopListening();
+      return this;
+    },
+
+    // Change the view's element (`this.el` property), including event
+    // re-delegation.
+    setElement: function(element, delegate) {
+      if (this.$el) this.undelegateEvents();
+      this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
+      this.el = this.$el[0];
+      if (delegate !== false) this.delegateEvents();
+      return this;
+    },
+
+    // Set callbacks, where `this.events` is a hash of
+    //
+    // *{"event selector": "callback"}*
+    //
+    //     {
+    //       'mousedown .title':  'edit',
+    //       'click .button':     'save',
+    //       'click .open':       function(e) { ... }
+    //     }
+    //
+    // pairs. Callbacks will be bound to the view, with `this` set properly.
+    // Uses event delegation for efficiency.
+    // Omitting the selector binds the event to `this.el`.
+    // This only works for delegate-able events: not `focus`, `blur`, and
+    // not `change`, `submit`, and `reset` in Internet Explorer.
+    delegateEvents: function(events) {
+      if (!(events || (events = _.result(this, 'events')))) return this;
+      this.undelegateEvents();
+      for (var key in events) {
+        var method = events[key];
+        if (!_.isFunction(method)) method = this[events[key]];
+        if (!method) continue;
+
+        var match = key.match(delegateEventSplitter);
+        var eventName = match[1], selector = match[2];
+        method = _.bind(method, this);
+        eventName += '.delegateEvents' + this.cid;
+        if (selector === '') {
+          this.$el.on(eventName, method);
+        } else {
+          this.$el.on(eventName, selector, method);
+        }
+      }
+      return this;
+    },
+
+    // Clears all callbacks previously bound to the view with `delegateEvents`.
+    // You usually don't need to use this, but may wish to if you have multiple
+    // Backbone views attached to the same DOM element.
+    undelegateEvents: function() {
+      this.$el.off('.delegateEvents' + this.cid);
+      return this;
+    },
+
+    // Ensure that the View has a DOM element to render into.
+    // If `this.el` is a string, pass it through `$()`, take the first
+    // matching element, and re-assign it to `el`. Otherwise, create
+    // an element from the `id`, `className` and `tagName` properties.
+    _ensureElement: function() {
+      if (!this.el) {
+        var attrs = _.extend({}, _.result(this, 'attributes'));
+        if (this.id) attrs.id = _.result(this, 'id');
+        if (this.className) attrs['class'] = _.result(this, 'className');
+        var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
+        this.setElement($el, false);
+      } else {
+        this.setElement(_.result(this, 'el'), false);
+      }
+    }
+
+  });
+
+  // Backbone.sync
+  // -------------
+
+  // Override this function to change the manner in which Backbone persists
+  // models to the server. You will be passed the type of request, and the
+  // model in question. By default, makes a RESTful Ajax request
+  // to the model's `url()`. Some possible customizations could be:
+  //
+  // * Use `setTimeout` to batch rapid-fire updates into a single request.
+  // * Send up the models as XML instead of JSON.
+  // * Persist models via WebSockets instead of Ajax.
+  //
+  // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
+  // as `POST`, with a `_method` parameter containing the true HTTP method,
+  // as well as all requests with the body as `application/x-www-form-urlencoded`
+  // instead of `application/json` with the model in a param named `model`.
+  // Useful when interfacing with server-side languages like **PHP** that make
+  // it difficult to read the body of `PUT` requests.
+  Backbone.sync = function(method, model, options) {
+    var type = methodMap[method];
+
+    // Default options, unless specified.
+    _.defaults(options || (options = {}), {
+      emulateHTTP: Backbone.emulateHTTP,
+      emulateJSON: Backbone.emulateJSON
+    });
+
+    // Default JSON-request options.
+    var params = {type: type, dataType: 'json'};
+
+    // Ensure that we have a URL.
+    if (!options.url) {
+      params.url = _.result(model, 'url') || urlError();
+    }
+
+    // Ensure that we have the appropriate request data.
+    if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
+      params.contentType = 'application/json';
+      params.data = JSON.stringify(options.attrs || model.toJSON(options));
+    }
+
+    // For older servers, emulate JSON by encoding the request into an HTML-form.
+    if (options.emulateJSON) {
+      params.contentType = 'application/x-www-form-urlencoded';
+      params.data = params.data ? {model: params.data} : {};
+    }
+
+    // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
+    // And an `X-HTTP-Method-Override` header.
+    if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
+      params.type = 'POST';
+      if (options.emulateJSON) params.data._method = type;
+      var beforeSend = options.beforeSend;
+      options.beforeSend = function(xhr) {
+        xhr.setRequestHeader('X-HTTP-Method-Override', type);
+        if (beforeSend) return beforeSend.apply(this, arguments);
+      };
+    }
+
+    // Don't process data on a non-GET request.
+    if (params.type !== 'GET' && !options.emulateJSON) {
+      params.processData = false;
+    }
+
+    // If we're sending a `PATCH` request, and we're in an old Internet Explorer
+    // that still has ActiveX enabled by default, override jQuery to use that
+    // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
+    if (params.type === 'PATCH' && noXhrPatch) {
+      params.xhr = function() {
+        return new ActiveXObject("Microsoft.XMLHTTP");
+      };
+    }
+
+    // Make the request, allowing the user to override any Ajax options.
+    var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
+    model.trigger('request', model, xhr, options);
+    return xhr;
+  };
+
+  var noXhrPatch =
+    typeof window !== 'undefined' && !!window.ActiveXObject &&
+      !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
+
+  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
+  var methodMap = {
+    'create': 'POST',
+    'update': 'PUT',
+    'patch':  'PATCH',
+    'delete': 'DELETE',
+    'read':   'GET'
+  };
+
+  // Set the default implementation of `Backbone.ajax` to proxy through to `$`.
+  // Override this if you'd like to use a different library.
+  Backbone.ajax = function() {
+    return Backbone.$.ajax.apply(Backbone.$, arguments);
+  };
+
+  // Backbone.Router
+  // ---------------
+
+  // Routers map faux-URLs to actions, and fire events when routes are
+  // matched. Creating a new one sets its `routes` hash, if not set statically.
+  var Router = Backbone.Router = function(options) {
+    options || (options = {});
+    if (options.routes) this.routes = options.routes;
+    this._bindRoutes();
+    this.initialize.apply(this, arguments);
+  };
+
+  // Cached regular expressions for matching named param parts and splatted
+  // parts of route strings.
+  var optionalParam = /\((.*?)\)/g;
+  var namedParam    = /(\(\?)?:\w+/g;
+  var splatParam    = /\*\w+/g;
+  var escapeRegExp  = /[\-{}\[\]+?.,\\\^$|#\s]/g;
+
+  // Set up all inheritable **Backbone.Router** properties and methods.
+  _.extend(Router.prototype, Events, {
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize: function(){},
+
+    // Manually bind a single named route to a callback. For example:
+    //
+    //     this.route('search/:query/p:num', 'search', function(query, num) {
+    //       ...
+    //     });
+    //
+    route: function(route, name, callback) {
+      if (!_.isRegExp(route)) route = this._routeToRegExp(route);
+      if (_.isFunction(name)) {
+        callback = name;
+        name = '';
+      }
+      if (!callback) callback = this[name];
+      var router = this;
+      Backbone.history.route(route, function(fragment) {
+        var args = router._extractParameters(route, fragment);
+        router.execute(callback, args);
+        router.trigger.apply(router, ['route:' + name].concat(args));
+        router.trigger('route', name, args);
+        Backbone.history.trigger('route', router, name, args);
+      });
+      return this;
+    },
+
+    // Execute a route handler with the provided parameters.  This is an
+    // excellent place to do pre-route setup or post-route cleanup.
+    execute: function(callback, args) {
+      if (callback) callback.apply(this, args);
+    },
+
+    // Simple proxy to `Backbone.history` to save a fragment into the history.
+    navigate: function(fragment, options) {
+      Backbone.history.navigate(fragment, options);
+      return this;
+    },
+
+    // Bind all defined routes to `Backbone.history`. We have to reverse the
+    // order of the routes here to support behavior where the most general
+    // routes can be defined at the bottom of the route map.
+    _bindRoutes: function() {
+      if (!this.routes) return;
+      this.routes = _.result(this, 'routes');
+      var route, routes = _.keys(this.routes);
+      while ((route = routes.pop()) != null) {
+        this.route(route, this.routes[route]);
+      }
+    },
+
+    // Convert a route string into a regular expression, suitable for matching
+    // against the current location hash.
+    _routeToRegExp: function(route) {
+      route = route.replace(escapeRegExp, '\\$&')
+                   .replace(optionalParam, '(?:$1)?')
+                   .replace(namedParam, function(match, optional) {
+                     return optional ? match : '([^/?]+)';
+                   })
+                   .replace(splatParam, '([^?]*?)');
+      return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
+    },
+
+    // Given a route, and a URL fragment that it matches, return the array of
+    // extracted decoded parameters. Empty or unmatched parameters will be
+    // treated as `null` to normalize cross-browser behavior.
+    _extractParameters: function(route, fragment) {
+      var params = route.exec(fragment).slice(1);
+      return _.map(params, function(param, i) {
+        // Don't decode the search params.
+        if (i === params.length - 1) return param || null;
+        return param ? decodeURIComponent(param) : null;
+      });
+    }
+
+  });
+
+  // Backbone.History
+  // ----------------
+
+  // Handles cross-browser history management, based on either
+  // [pushState](http://diveintohtml5.info/history.html) and real URLs, or
+  // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
+  // and URL fragments. If the browser supports neither (old IE, natch),
+  // falls back to polling.
+  var History = Backbone.History = function() {
+    this.handlers = [];
+    _.bindAll(this, 'checkUrl');
+
+    // Ensure that `History` can be used outside of the browser.
+    if (typeof window !== 'undefined') {
+      this.location = window.location;
+      this.history = window.history;
+    }
+  };
+
+  // Cached regex for stripping a leading hash/slash and trailing space.
+  var routeStripper = /^[#\/]|\s+$/g;
+
+  // Cached regex for stripping leading and trailing slashes.
+  var rootStripper = /^\/+|\/+$/g;
+
+  // Cached regex for detecting MSIE.
+  var isExplorer = /msie [\w.]+/;
+
+  // Cached regex for removing a trailing slash.
+  var trailingSlash = /\/$/;
+
+  // Cached regex for stripping urls of hash.
+  var pathStripper = /#.*$/;
+
+  // Has the history handling already been started?
+  History.started = false;
+
+  // Set up all inheritable **Backbone.History** properties and methods.
+  _.extend(History.prototype, Events, {
+
+    // The default interval to poll for hash changes, if necessary, is
+    // twenty times a second.
+    interval: 50,
+
+    // Are we at the app root?
+    atRoot: function() {
+      return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root;
+    },
+
+    // Gets the true hash value. Cannot use location.hash directly due to bug
+    // in Firefox where location.hash will always be decoded.
+    getHash: function(window) {
+      var match = (window || this).location.href.match(/#(.*)$/);
+      return match ? match[1] : '';
+    },
+
+    // Get the cross-browser normalized URL fragment, either from the URL,
+    // the hash, or the override.
+    getFragment: function(fragment, forcePushState) {
+      if (fragment == null) {
+        if (this._hasPushState || !this._wantsHashChange || forcePushState) {
+          fragment = decodeURI(this.location.pathname + this.location.search);
+          var root = this.root.replace(trailingSlash, '');
+          if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
+        } else {
+          fragment = this.getHash();
+        }
+      }
+      return fragment.replace(routeStripper, '');
+    },
+
+    // Start the hash change handling, returning `true` if the current URL matches
+    // an existing route, and `false` otherwise.
+    start: function(options) {
+      if (History.started) throw new Error("Backbone.history has already been started");
+      History.started = true;
+
+      // Figure out the initial configuration. Do we need an iframe?
+      // Is pushState desired ... is it available?
+      this.options          = _.extend({root: '/'}, this.options, options);
+      this.root             = this.options.root;
+      this._wantsHashChange = this.options.hashChange !== false;
+      this._wantsPushState  = !!this.options.pushState;
+      this._hasPushState    = !!(this.options.pushState && this.history && this.history.pushState);
+      var fragment          = this.getFragment();
+      var docMode           = document.documentMode;
+      var oldIE             = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
+
+      // Normalize root to always include a leading and trailing slash.
+      this.root = ('/' + this.root + '/').replace(rootStripper, '/');
+
+      if (oldIE && this._wantsHashChange) {
+        var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">');
+        this.iframe = frame.hide().appendTo('body')[0].contentWindow;
+        this.navigate(fragment);
+      }
+
+      // Depending on whether we're using pushState or hashes, and whether
+      // 'onhashchange' is supported, determine how we check the URL state.
+      if (this._hasPushState) {
+        Backbone.$(window).on('popstate', this.checkUrl);
+      } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
+        Backbone.$(window).on('hashchange', this.checkUrl);
+      } else if (this._wantsHashChange) {
+        this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
+      }
+
+      // Determine if we need to change the base url, for a pushState link
+      // opened by a non-pushState browser.
+      this.fragment = fragment;
+      var loc = this.location;
+
+      // Transition from hashChange to pushState or vice versa if both are
+      // requested.
+      if (this._wantsHashChange && this._wantsPushState) {
+
+        // If we've started off with a route from a `pushState`-enabled
+        // browser, but we're currently in a browser that doesn't support it...
+        if (!this._hasPushState && !this.atRoot()) {
+          this.fragment = this.getFragment(null, true);
+          this.location.replace(this.root + '#' + this.fragment);
+          // Return immediately as browser will do redirect to new url
+          return true;
+
+        // Or if we've started out with a hash-based route, but we're currently
+        // in a browser where it could be `pushState`-based instead...
+        } else if (this._hasPushState && this.atRoot() && loc.hash) {
+          this.fragment = this.getHash().replace(routeStripper, '');
+          this.history.replaceState({}, document.title, this.root + this.fragment);
+        }
+
+      }
+
+      if (!this.options.silent) return this.loadUrl();
+    },
+
+    // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
+    // but possibly useful for unit testing Routers.
+    stop: function() {
+      Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
+      if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
+      History.started = false;
+    },
+
+    // Add a route to be tested when the fragment changes. Routes added later
+    // may override previous routes.
+    route: function(route, callback) {
+      this.handlers.unshift({route: route, callback: callback});
+    },
+
+    // Checks the current URL to see if it has changed, and if it has,
+    // calls `loadUrl`, normalizing across the hidden iframe.
+    checkUrl: function(e) {
+      var current = this.getFragment();
+      if (current === this.fragment && this.iframe) {
+        current = this.getFragment(this.getHash(this.iframe));
+      }
+      if (current === this.fragment) return false;
+      if (this.iframe) this.navigate(current);
+      this.loadUrl();
+    },
+
+    // Attempt to load the current URL fragment. If a route succeeds with a
+    // match, returns `true`. If no defined routes matches the fragment,
+    // returns `false`.
+    loadUrl: function(fragment) {
+      fragment = this.fragment = this.getFragment(fragment);
+      return _.any(this.handlers, function(handler) {
+        if (handler.route.test(fragment)) {
+          handler.callback(fragment);
+          return true;
+        }
+      });
+    },
+
+    // Save a fragment into the hash history, or replace the URL state if the
+    // 'replace' option is passed. You are responsible for properly URL-encoding
+    // the fragment in advance.
+    //
+    // The options object can contain `trigger: true` if you wish to have the
+    // route callback be fired (not usually desirable), or `replace: true`, if
+    // you wish to modify the current URL without adding an entry to the history.
+    navigate: function(fragment, options) {
+      if (!History.started) return false;
+      if (!options || options === true) options = {trigger: !!options};
+
+      var url = this.root + (fragment = this.getFragment(fragment || ''));
+
+      // Strip the hash for matching.
+      fragment = fragment.replace(pathStripper, '');
+
+      if (this.fragment === fragment) return;
+      this.fragment = fragment;
+
+      // Don't include a trailing slash on the root.
+      if (fragment === '' && url !== '/') url = url.slice(0, -1);
+
+      // If pushState is available, we use it to set the fragment as a real URL.
+      if (this._hasPushState) {
+        this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
+
+      // If hash changes haven't been explicitly disabled, update the hash
+      // fragment to store history.
+      } else if (this._wantsHashChange) {
+        this._updateHash(this.location, fragment, options.replace);
+        if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
+          // Opening and closing the iframe tricks IE7 and earlier to push a
+          // history entry on hash-tag change.  When replace is true, we don't
+          // want this.
+          if(!options.replace) this.iframe.document.open().close();
+          this._updateHash(this.iframe.location, fragment, options.replace);
+        }
+
+      // If you've told us that you explicitly don't want fallback hashchange-
+      // based history, then `navigate` becomes a page refresh.
+      } else {
+        return this.location.assign(url);
+      }
+      if (options.trigger) return this.loadUrl(fragment);
+    },
+
+    // Update the hash location, either replacing the current entry, or adding
+    // a new one to the browser history.
+    _updateHash: function(location, fragment, replace) {
+      if (replace) {
+        var href = location.href.replace(/(javascript:|#).*$/, '');
+        location.replace(href + '#' + fragment);
+      } else {
+        // Some browsers require that `hash` contains a leading #.
+        location.hash = '#' + fragment;
+      }
+    }
+
+  });
+
+  // Create the default Backbone.history.
+  Backbone.history = new History;
+
+  // Helpers
+  // -------
+
+  // Helper function to correctly set up the prototype chain, for subclasses.
+  // Similar to `goog.inherits`, but uses a hash of prototype properties and
+  // class properties to be extended.
+  var extend = function(protoProps, staticProps) {
+    var parent = this;
+    var child;
+
+    // The constructor function for the new subclass is either defined by you
+    // (the "constructor" property in your `extend` definition), or defaulted
+    // by us to simply call the parent's constructor.
+    if (protoProps && _.has(protoProps, 'constructor')) {
+      child = protoProps.constructor;
+    } else {
+      child = function(){ return parent.apply(this, arguments); };
+    }
+
+    // Add static properties to the constructor function, if supplied.
+    _.extend(child, parent, staticProps);
+
+    // Set the prototype chain to inherit from `parent`, without calling
+    // `parent`'s constructor function.
+    var Surrogate = function(){ this.constructor = child; };
+    Surrogate.prototype = parent.prototype;
+    child.prototype = new Surrogate;
+
+    // Add prototype properties (instance properties) to the subclass,
+    // if supplied.
+    if (protoProps) _.extend(child.prototype, protoProps);
+
+    // Set a convenience property in case the parent's prototype is needed
+    // later.
+    child.__super__ = parent.prototype;
+
+    return child;
+  };
+
+  // Set up inheritance for the model, collection, router, view and history.
+  Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
+
+  // Throw an error when a URL is needed, and none is supplied.
+  var urlError = function() {
+    throw new Error('A "url" property or function must be specified');
+  };
+
+  // Wrap an optional error callback with a fallback error event.
+  var wrapError = function(model, options) {
+    var error = options.error;
+    options.error = function(resp) {
+      if (error) error(model, resp, options);
+      model.trigger('error', model, resp, options);
+    };
+  };
+
+  return Backbone;
+
+}));
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/backbone-1.1.2.min.js b/htrace-core/src/web/lib/js/backbone-1.1.2.min.js
deleted file mode 100644
index 6dbb695..0000000
--- a/htrace-core/src/web/lib/js/backbone-1.1.2.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-(function(t,e){if(typeof define==="function"&&define.amd){define(["underscore","jquery","exports"],function(i,r,s){t.Backbone=e(t,s,i,r)})}else if(typeof exports!=="undefined"){var i=require("underscore");e(t,exports,i)}else{t.Backbone=e(t,{},t._,t.jQuery||t.Zepto||t.ender||t.$)}})(this,function(t,e,i,r){var s=t.Backbone;var n=[];var a=n.push;var o=n.slice;var h=n.splice;e.VERSION="1.1.2";e.$=r;e.noConflict=function(){t.Backbone=s;return this};e.emulateHTTP=false;e.emulateJSON=false;var u=e.Events={on:function(t,e,i){if(!c(this,"on",t,[e,i])||!e)return this;this._events||(this._events={});var r=this._events[t]||(this._events[t]=[]);r.push({callback:e,context:i,ctx:i||this});return this},once:function(t,e,r){if(!c(this,"once",t,[e,r])||!e)return this;var s=this;var n=i.once(function(){s.off(t,n);e.apply(this,arguments)});n._callback=e;return this.on(t,n,r)},off:function(t,e,r){var s,n,a,o,h,u,l,f;if(!this._events||!c(this,"off",t,[e,r]))return this;if(!t&&!e&&!r){this._events=void 0;return this}o=t?[t]:i.keys(this._events);for(h=0,u=o.length;h<u;h++){t=o[h];if(a=this._events[t]){this._events[t]=s=[];if(e||r){for(l=0,f=a.length;l<f;l++){n=a[l];if(e&&e!==n.callback&&e!==n.callback._callback||r&&r!==n.context){s.push(n)}}}if(!s.length)delete this._events[t]}}return this},trigger:function(t){if(!this._events)return this;var e=o.call(arguments,1);if(!c(this,"trigger",t,e))return this;var i=this._events[t];var r=this._events.all;if(i)f(i,e);if(r)f(r,arguments);return this},stopListening:function(t,e,r){var s=this._listeningTo;if(!s)return this;var n=!e&&!r;if(!r&&typeof e==="object")r=this;if(t)(s={})[t._listenId]=t;for(var a in s){t=s[a];t.off(e,r,this);if(n||i.isEmpty(t._events))delete this._listeningTo[a]}return this}};var l=/\s+/;var c=function(t,e,i,r){if(!i)return true;if(typeof i==="object"){for(var s in i){t[e].apply(t,[s,i[s]].concat(r))}return false}if(l.test(i)){var n=i.split(l);for(var a=0,o=n.length;a<o;a++){t[e].apply(t,[n[a]].concat(r))}return false}return true};var f=function(t,e){var i,r=-1,s=t.length,n=e[0],a=e[1],o=e[2];switch(e.length){case 0:while(++r<s)(i=t[r]).callback.call(i.ctx);return;case 1:while(++r<s)(i=t[r]).callback.call(i.ctx,n);return;case 2:while(++r<s)(i=t[r]).callback.call(i.ctx,n,a);return;case 3:while(++r<s)(i=t[r]).callback.call(i.ctx,n,a,o);return;default:while(++r<s)(i=t[r]).callback.apply(i.ctx,e);return}};var d={listenTo:"on",listenToOnce:"once"};i.each(d,function(t,e){u[e]=function(e,r,s){var n=this._listeningTo||(this._listeningTo={});var a=e._listenId||(e._listenId=i.uniqueId("l"));n[a]=e;if(!s&&typeof r==="object")s=this;e[t](r,s,this);return this}});u.bind=u.on;u.unbind=u.off;i.extend(e,u);var p=e.Model=function(t,e){var r=t||{};e||(e={});this.cid=i.uniqueId("c");this.attributes={};if(e.collection)this.collection=e.collection;if(e.parse)r=this.parse(r,e)||{};r=i.defaults({},r,i.result(this,"defaults"));this.set(r,e);this.changed={};this.initialize.apply(this,arguments)};i.extend(p.prototype,u,{changed:null,validationError:null,idAttribute:"id",initialize:function(){},toJSON:function(t){return i.clone(this.attributes)},sync:function(){return e.sync.apply(this,arguments)},get:function(t){return this.attributes[t]},escape:function(t){return i.escape(this.get(t))},has:function(t){return this.get(t)!=null},set:function(t,e,r){var s,n,a,o,h,u,l,c;if(t==null)return this;if(typeof t==="object"){n=t;r=e}else{(n={})[t]=e}r||(r={});if(!this._validate(n,r))return false;a=r.unset;h=r.silent;o=[];u=this._changing;this._changing=true;if(!u){this._previousAttributes=i.clone(this.attributes);this.changed={}}c=this.attributes,l=this._previousAttributes;if(this.idAttribute in n)this.id=n[this.idAttribute];for(s in n){e=n[s];if(!i.isEqual(c[s],e))o.push(s);if(!i.isEqual(l[s],e)){this.changed[s]=e}else{delete this.changed[s]}a?delete c[s]:c[s]=e}if(!h){if(o.length)this._pending=r;for(var f=0,d=o.length;f<d;f++){this.trigger("change:"+o[f],this,c[o[f]],r)}}if(u)return this;if(!h){while(this._pending){r=this._pending;this._pending=false;this.trigger("change",this,r)}}this._pending=false;this._changing=false;return this},unset:function(t,e){return this.set(t,void 0,i.extend({},e,{unset:true}))},clear:function(t){var e={};for(var r in this.attributes)e[r]=void 0;return this.set(e,i.extend({},t,{unset:true}))},hasChanged:function(t){if(t==null)return!i.isEmpty(this.changed);return i.has(this.changed,t)},changedAttributes:function(t){if(!t)return this.hasChanged()?i.clone(this.changed):false;var e,r=false;var s=this._changing?this._previousAttributes:this.attributes;for(var n in t){if(i.isEqual(s[n],e=t[n]))continue;(r||(r={}))[n]=e}return r},previous:function(t){if(t==null||!this._previousAttributes)return null;return this._previousAttributes[t]},previousAttributes:function(){return i.clone(this._previousAttributes)},fetch:function(t){t=t?i.clone(t):{};if(t.parse===void 0)t.parse=true;var e=this;var r=t.success;t.success=function(i){if(!e.set(e.parse(i,t),t))return false;if(r)r(e,i,t);e.trigger("sync",e,i,t)};q(this,t);return this.sync("read",this,t)},save:function(t,e,r){var s,n,a,o=this.attributes;if(t==null||typeof t==="object"){s=t;r=e}else{(s={})[t]=e}r=i.extend({validate:true},r);if(s&&!r.wait){if(!this.set(s,r))return false}else{if(!this._validate(s,r))return false}if(s&&r.wait){this.attributes=i.extend({},o,s)}if(r.parse===void 0)r.parse=true;var h=this;var u=r.success;r.success=function(t){h.attributes=o;var e=h.parse(t,r);if(r.wait)e=i.extend(s||{},e);if(i.isObject(e)&&!h.set(e,r)){return false}if(u)u(h,t,r);h.trigger("sync",h,t,r)};q(this,r);n=this.isNew()?"create":r.patch?"patch":"update";if(n==="patch")r.attrs=s;a=this.sync(n,this,r);if(s&&r.wait)this.attributes=o;return a},destroy:function(t){t=t?i.clone(t):{};var e=this;var r=t.success;var s=function(){e.trigger("destroy",e,e.collection,t)};t.success=function(i){if(t.wait||e.isNew())s();if(r)r(e,i,t);if(!e.isNew())e.trigger("sync",e,i,t)};if(this.isNew()){t.success();return false}q(this,t);var n=this.sync("delete",this,t);if(!t.wait)s();return n},url:function(){var t=i.result(this,"urlRoot")||i.result(this.collection,"url")||M();if(this.isNew())return t;return t.replace(/([^\/])$/,"$1/")+encodeURIComponent(this.id)},parse:function(t,e){return t},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return!this.has(this.idAttribute)},isValid:function(t){return this._validate({},i.extend(t||{},{validate:true}))},_validate:function(t,e){if(!e.validate||!this.validate)return true;t=i.extend({},this.attributes,t);var r=this.validationError=this.validate(t,e)||null;if(!r)return true;this.trigger("invalid",this,r,i.extend(e,{validationError:r}));return false}});var v=["keys","values","pairs","invert","pick","omit"];i.each(v,function(t){p.prototype[t]=function(){var e=o.call(arguments);e.unshift(this.attributes);return i[t].apply(i,e)}});var g=e.Collection=function(t,e){e||(e={});if(e.model)this.model=e.model;if(e.comparator!==void 0)this.comparator=e.comparator;this._reset();this.initialize.apply(this,arguments);if(t)this.reset(t,i.extend({silent:true},e))};var m={add:true,remove:true,merge:true};var y={add:true,remove:false};i.extend(g.prototype,u,{model:p,initialize:function(){},toJSON:function(t){return this.map(function(e){return e.toJSON(t)})},sync:function(){return e.sync.apply(this,arguments)},add:function(t,e){return this.set(t,i.extend({merge:false},e,y))},remove:function(t,e){var r=!i.isArray(t);t=r?[t]:i.clone(t);e||(e={});var s,n,a,o;for(s=0,n=t.length;s<n;s++){o=t[s]=this.get(t[s]);if(!o)continue;delete this._byId[o.id];delete this._byId[o.cid];a=this.indexOf(o);this.models.splice(a,1);this.length--;if(!e.silent){e.index=a;o.trigger("remove",o,this,e)}this._removeReference(o,e)}return r?t[0]:t},set:function(t,e){e=i.defaults({},e,m);if(e.parse)t=this.parse(t,e);var r=!i.isArray(t);t=r?t?[t]:[]:i.clone(t);var s,n,a,o,h,u,l;var c=e.at;var f=this.model;var d=this.comparator&&c==null&&e.sort!==false;var v=i.isString(this.comparator)?this.comparator:null;var g=[],y=[],_={};var b=e.add,w=e.merge,x=e.remove;var E=!d&&b&&x?[]:false;for(s=0,n=t.length;s<n;s++){h=t[s]||{};if(h instanceof p){a=o=h}else{a=h[f.prototype.idAttribute||"id"]}if(u=this.get(a)){if(x)_[u.cid]=true;if(w){h=h===o?o.attributes:h;if(e.parse)h=u.parse(h,e);u.set(h,e);if(d&&!l&&u.hasChanged(v))l=true}t[s]=u}else if(b){o=t[s]=this._prepareModel(h,e);if(!o)continue;g.push(o);this._addReference(o,e)}o=u||o;if(E&&(o.isNew()||!_[o.id]))E.push(o);_[o.id]=true}if(x){for(s=0,n=this.length;s<n;++s){if(!_[(o=this.models[s]).cid])y.push(o)}if(y.length)this.remove(y,e)}if(g.length||E&&E.length){if(d)l=true;this.length+=g.length;if(c!=null){for(s=0,n=g.length;s<n;s++){this.models.splice(c+s,0,g[s])}}else{if(E)this.models.length=0;var k=E||g;for(s=0,n=k.length;s<n;s++){this.models.push(k[s])}}}if(l)this.sort({silent:true});if(!e.silent){for(s=0,n=g.length;s<n;s++){(o=g[s]).trigger("add",o,this,e)}if(l||E&&E.length)this.trigger("sort",this,e)}return r?t[0]:t},reset:function(t,e){e||(e={});for(var r=0,s=this.models.length;r<s;r++){this._removeReference(this.models[r],e)}e.previousModels=this.models;this._reset();t=this.add(t,i.extend({silent:true},e));if(!e.silent)this.trigger("reset",this,e);return t},push:function(t,e){return this.add(t,i.extend({at:this.length},e))},pop:function(t){var e=this.at(this.length-1);this.remove(e,t);return e},unshift:function(t,e){return this.add(t,i.extend({at:0},e))},shift:function(t){var e=this.at(0);this.remove(e,t);return e},slice:function(){return o.apply(this.models,arguments)},get:function(t){if(t==null)return void 0;return this._byId[t]||this._byId[t.id]||this._byId[t.cid]},at:function(t){return this.models[t]},where:function(t,e){if(i.isEmpty(t))return e?void 0:[];return this[e?"find":"filter"](function(e){for(var i in t){if(t[i]!==e.get(i))return false}return true})},findWhere:function(t){return this.where(t,true)},sort:function(t){if(!this.comparator)throw new Error("Cannot sort a set without a comparator");t||(t={});if(i.isString(this.comparator)||this.comparator.length===1){this.models=this.sortBy(this.comparator,this)}else{this.models.sort(i.bind(this.comparator,this))}if(!t.silent)this.trigger("sort",this,t);return this},pluck:function(t){return i.invoke(this.models,"get",t)},fetch:function(t){t=t?i.clone(t):{};if(t.parse===void 0)t.parse=true;var e=t.success;var r=this;t.success=function(i){var s=t.reset?"reset":"set";r[s](i,t);if(e)e(r,i,t);r.trigger("sync",r,i,t)};q(this,t);return this.sync("read",this,t)},create:function(t,e){e=e?i.clone(e):{};if(!(t=this._prepareModel(t,e)))return false;if(!e.wait)this.add(t,e);var r=this;var s=e.success;e.success=function(t,i){if(e.wait)r.add(t,e);if(s)s(t,i,e)};t.save(null,e);return t},parse:function(t,e){return t},clone:function(){return new this.constructor(this.models)},_reset:function(){this.length=0;this.models=[];this._byId={}},_prepareModel:function(t,e){if(t instanceof p)return t;e=e?i.clone(e):{};e.collection=this;var r=new this.model(t,e);if(!r.validationError)return r;this.trigger("invalid",this,r.validationError,e);return false},_addReference:function(t,e){this._byId[t.cid]=t;if(t.id!=null)this._byId[t.id]=t;if(!t.collection)t.collection=this;t.on("all",this._onModelEvent,this)},_removeReference:function(t,e){if(this===t.collection)delete t.collection;t.off("all",this._onModelEvent,this)},_onModelEvent:function(t,e,i,r){if((t==="add"||t==="remove")&&i!==this)return;if(t==="destroy")this.remove(e,r);if(e&&t==="change:"+e.idAttribute){delete this._byId[e.previous(e.idAttribute)];if(e.id!=null)this._byId[e.id]=e}this.trigger.apply(this,arguments)}});var _=["forEach","each","map","collect","reduce","foldl","inject","reduceRight","foldr","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","max","min","toArray","size","first","head","take","initial","rest","tail","drop","last","without","difference","indexOf","shuffle","lastIndexOf","isEmpty","chain","sample"];i.each(_,function(t){g.prototype[t]=function(){var e=o.call(arguments);e.unshift(this.models);return i[t].apply(i,e)}});var b=["groupBy","countBy","sortBy","indexBy"];i.each(b,function(t){g.prototype[t]=function(e,r){var s=i.isFunction(e)?e:function(t){return t.get(e)};return i[t](this.models,s,r)}});var w=e.View=function(t){this.cid=i.uniqueId("view");t||(t={});i.extend(this,i.pick(t,E));this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()};var x=/^(\S+)\s*(.*)$/;var E=["model","collection","el","id","attributes","className","tagName","events"];i.extend(w.prototype,u,{tagName:"div",$:function(t){return this.$el.find(t)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();this.stopListening();return this},setElement:function(t,i){if(this.$el)this.undelegateEvents();this.$el=t instanceof e.$?t:e.$(t);this.el=this.$el[0];if(i!==false)this.delegateEvents();return this},delegateEvents:function(t){if(!(t||(t=i.result(this,"events"))))return this;this.undelegateEvents();for(var e in t){var r=t[e];if(!i.isFunction(r))r=this[t[e]];if(!r)continue;var s=e.match(x);var n=s[1],a=s[2];r=i.bind(r,this);n+=".delegateEvents"+this.cid;if(a===""){this.$el.on(n,r)}else{this.$el.on(n,a,r)}}return this},undelegateEvents:function(){this.$el.off(".delegateEvents"+this.cid);return this},_ensureElement:function(){if(!this.el){var t=i.extend({},i.result(this,"attributes"));if(this.id)t.id=i.result(this,"id");if(this.className)t["class"]=i.result(this,"className");var r=e.$("<"+i.result(this,"tagName")+">").attr(t);this.setElement(r,false)}else{this.setElement(i.result(this,"el"),false)}}});e.sync=function(t,r,s){var n=T[t];i.defaults(s||(s={}),{emulateHTTP:e.emulateHTTP,emulateJSON:e.emulateJSON});var a={type:n,dataType:"json"};if(!s.url){a.url=i.result(r,"url")||M()}if(s.data==null&&r&&(t==="create"||t==="update"||t==="patch")){a.contentType="application/json";a.data=JSON.stringify(s.attrs||r.toJSON(s))}if(s.emulateJSON){a.contentType="application/x-www-form-urlencoded";a.data=a.data?{model:a.data}:{}}if(s.emulateHTTP&&(n==="PUT"||n==="DELETE"||n==="PATCH")){a.type="POST";if(s.emulateJSON)a.data._method=n;var o=s.beforeSend;s.beforeSend=function(t){t.setRequestHeader("X-HTTP-Method-Override",n);if(o)return o.apply(this,arguments)}}if(a.type!=="GET"&&!s.emulateJSON){a.processData=false}if(a.type==="PATCH"&&k){a.xhr=function(){return new ActiveXObject("Microsoft.XMLHTTP")}}var h=s.xhr=e.ajax(i.extend(a,s));r.trigger("request",r,h,s);return h};var k=typeof window!=="undefined"&&!!window.ActiveXObject&&!(window.XMLHttpRequest&&(new XMLHttpRequest).dispatchEvent);var T={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};e.ajax=function(){return e.$.ajax.apply(e.$,arguments)};var $=e.Router=function(t){t||(t={});if(t.routes)this.routes=t.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var S=/\((.*?)\)/g;var H=/(\(\?)?:\w+/g;var A=/\*\w+/g;var I=/[\-{}\[\]+?.,\\\^$|#\s]/g;i.extend($.prototype,u,{initialize:function(){},route:function(t,r,s){if(!i.isRegExp(t))t=this._routeToRegExp(t);if(i.isFunction(r)){s=r;r=""}if(!s)s=this[r];var n=this;e.history.route(t,function(i){var a=n._extractParameters(t,i);n.execute(s,a);n.trigger.apply(n,["route:"+r].concat(a));n.trigger("route",r,a);e.history.trigger("route",n,r,a)});return this},execute:function(t,e){if(t)t.apply(this,e)},navigate:function(t,i){e.history.navigate(t,i);return this},_bindRoutes:function(){if(!this.routes)return;this.routes=i.result(this,"routes");var t,e=i.keys(this.routes);while((t=e.pop())!=null){this.route(t,this.routes[t])}},_routeToRegExp:function(t){t=t.replace(I,"\\$&").replace(S,"(?:$1)?").replace(H,function(t,e){return e?t:"([^/?]+)"}).replace(A,"([^?]*?)");return new RegExp("^"+t+"(?:\\?([\\s\\S]*))?$")},_extractParameters:function(t,e){var r=t.exec(e).slice(1);return i.map(r,function(t,e){if(e===r.length-1)return t||null;return t?decodeURIComponent(t):null})}});var N=e.History=function(){this.handlers=[];i.bindAll(this,"checkUrl");if(typeof window!=="undefined"){this.location=window.location;this.history=window.history}};var R=/^[#\/]|\s+$/g;var O=/^\/+|\/+$/g;var P=/msie [\w.]+/;var C=/\/$/;var j=/#.*$/;N.started=false;i.extend(N.prototype,u,{interval:50,atRoot:function(){return this.location.pathname.replace(/[^\/]$/,"$&/")===this.root},getHash:function(t){var e=(t||this).location.href.match(/#(.*)$/);return e?e[1]:""},getFragment:function(t,e){if(t==null){if(this._hasPushState||!this._wantsHashChange||e){t=decodeURI(this.location.pathname+this.location.search);var i=this.root.replace(C,"");if(!t.indexOf(i))t=t.slice(i.length)}else{t=this.getHash()}}return t.replace(R,"")},start:function(t){if(N.started)throw new Error("Backbone.history has already been started");N.started=true;this.options=i.extend({root:"/"},this.options,t);this.root=this.options.root;this._wantsHashChange=this.options.hashChange!==false;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.options.pushState&&this.history&&this.history.pushState);var r=this.getFragment();var s=document.documentMode;var n=P.exec(navigator.userAgent.toLowerCase())&&(!s||s<=7);this.root=("/"+this.root+"/").replace(O,"/");if(n&&this._wantsHashChange){var a=e.$('<iframe src="javascript:0" tabindex="-1">');this.iframe=a.hide().appendTo("body")[0].contentWindow;this.navigate(r)}if(this._hasPushState){e.$(window).on("popstate",this.checkUrl)}else if(this._wantsHashChange&&"onhashchange"in window&&!n){e.$(window).on("hashchange",this.checkUrl)}else if(this._wantsHashChange){this._checkUrlInterval=setInterval(this.checkUrl,this.interval)}this.fragment=r;var o=this.location;if(this._wantsHashChange&&this._wantsPushState){if(!this._hasPushState&&!this.atRoot()){this.fragment=this.getFragment(null,true);this.location.replace(this.root+"#"+this.fragment);return true}else if(this._hasPushState&&this.atRoot()&&o.hash){this.fragment=this.getHash().replace(R,"");this.history.replaceState({},document.title,this.root+this.fragment)}}if(!this.options.silent)return this.loadUrl()},stop:function(){e.$(window).off("popstate",this.checkUrl).off("hashchange",this.checkUrl);if(this._checkUrlInterval)clearInterval(this._checkUrlInterval);N.started=false},route:function(t,e){this.handlers.unshift({route:t,callback:e})},checkUrl:function(t){var e=this.getFragment();if(e===this.fragment&&this.iframe){e=this.getFragment(this.getHash(this.iframe))}if(e===this.fragment)return false;if(this.iframe)this.navigate(e);this.loadUrl()},loadUrl:function(t){t=this.fragment=this.getFragment(t);return i.any(this.handlers,function(e){if(e.route.test(t)){e.callback(t);return true}})},navigate:function(t,e){if(!N.started)return false;if(!e||e===true)e={trigger:!!e};var i=this.root+(t=this.getFragment(t||""));t=t.replace(j,"");if(this.fragment===t)return;this.fragment=t;if(t===""&&i!=="/")i=i.slice(0,-1);if(this._hasPushState){this.history[e.replace?"replaceState":"pushState"]({},document.title,i)}else if(this._wantsHashChange){this._updateHash(this.location,t,e.replace);if(this.iframe&&t!==this.getFragment(this.getHash(this.iframe))){if(!e.replace)this.iframe.document.open().close();this._updateHash(this.iframe.location,t,e.replace)}}else{return this.location.assign(i)}if(e.trigger)return this.loadUrl(t)},_updateHash:function(t,e,i){if(i){var r=t.href.replace(/(javascript:|#).*$/,"");t.replace(r+"#"+e)}else{t.hash="#"+e}}});e.history=new N;var U=function(t,e){var r=this;var s;if(t&&i.has(t,"constructor")){s=t.constructor}else{s=function(){return r.apply(this,arguments)}}i.extend(s,r,e);var n=function(){this.constructor=s};n.prototype=r.prototype;s.prototype=new n;if(t)i.extend(s.prototype,t);s.__super__=r.prototype;return s};p.extend=g.extend=$.extend=w.extend=N.extend=U;var M=function(){throw new Error('A "url" property or function must be specified')};var q=function(t,e){var i=e.error;e.error=function(r){if(i)i(t,r,e);t.trigger("error",t,r,e)}};return e});
-//# sourceMappingURL=backbone-min.map
diff --git a/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.js b/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.js
new file mode 100644
index 0000000..d8ccc65
--- /dev/null
+++ b/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.js
@@ -0,0 +1,1325 @@
+/*
+  backbone.paginator 2.0.0
+  http://github.com/backbone-paginator/backbone.paginator
+
+  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+  Licensed under the MIT @license.
+*/
+
+(function (factory) {
+
+  // CommonJS
+  if (typeof exports == "object") {
+    module.exports = factory(require("underscore"), require("backbone"));
+  }
+  // AMD
+  else if (typeof define == "function" && define.amd) {
+    define(["underscore", "backbone"], factory);
+  }
+  // Browser
+  else if (typeof _ !== "undefined" && typeof Backbone !== "undefined") {
+    var oldPageableCollection = Backbone.PageableCollection;
+    var PageableCollection = factory(_, Backbone);
+
+    /**
+       __BROWSER ONLY__
+
+       If you already have an object named `PageableCollection` attached to the
+       `Backbone` module, you can use this to return a local reference to this
+       Backbone.PageableCollection class and reset the name
+       Backbone.PageableCollection to its previous definition.
+
+           // The left hand side gives you a reference to this
+           // Backbone.PageableCollection implementation, the right hand side
+           // resets Backbone.PageableCollection to your other
+           // Backbone.PageableCollection.
+           var PageableCollection = Backbone.PageableCollection.noConflict();
+
+       @static
+       @member Backbone.PageableCollection
+       @return {Backbone.PageableCollection}
+    */
+    Backbone.PageableCollection.noConflict = function () {
+      Backbone.PageableCollection = oldPageableCollection;
+      return PageableCollection;
+    };
+  }
+
+}(function (_, Backbone) {
+
+  "use strict";
+
+  var _extend = _.extend;
+  var _omit = _.omit;
+  var _clone = _.clone;
+  var _each = _.each;
+  var _pick = _.pick;
+  var _contains = _.contains;
+  var _isEmpty = _.isEmpty;
+  var _pairs = _.pairs;
+  var _invert = _.invert;
+  var _isArray = _.isArray;
+  var _isFunction = _.isFunction;
+  var _isObject = _.isObject;
+  var _keys = _.keys;
+  var _isUndefined = _.isUndefined;
+  var ceil = Math.ceil;
+  var floor = Math.floor;
+  var max = Math.max;
+
+  var BBColProto = Backbone.Collection.prototype;
+
+  function finiteInt (val, name) {
+    if (!_.isNumber(val) || _.isNaN(val) || !_.isFinite(val) || ~~val !== val) {
+      throw new TypeError("`" + name + "` must be a finite integer");
+    }
+    return val;
+  }
+
+  function queryStringToParams (qs) {
+    var kvp, k, v, ls, params = {}, decode = decodeURIComponent;
+    var kvps = qs.split('&');
+    for (var i = 0, l = kvps.length; i < l; i++) {
+      var param = kvps[i];
+      kvp = param.split('='), k = kvp[0], v = kvp[1] || true;
+      k = decode(k), v = decode(v), ls = params[k];
+      if (_isArray(ls)) ls.push(v);
+      else if (ls) params[k] = [ls, v];
+      else params[k] = v;
+    }
+    return params;
+  }
+
+  // hack to make sure the whatever event handlers for this event is run
+  // before func is, and the event handlers that func will trigger.
+  function runOnceAtLastHandler (col, event, func) {
+    var eventHandlers = col._events[event];
+    if (eventHandlers && eventHandlers.length) {
+      var lastHandler = eventHandlers[eventHandlers.length - 1];
+      var oldCallback = lastHandler.callback;
+      lastHandler.callback = function () {
+        try {
+          oldCallback.apply(this, arguments);
+          func();
+        }
+        catch (e) {
+          throw e;
+        }
+        finally {
+          lastHandler.callback = oldCallback;
+        }
+      };
+    }
+    else func();
+  }
+
+  var PARAM_TRIM_RE = /[\s'"]/g;
+  var URL_TRIM_RE = /[<>\s'"]/g;
+
+  /**
+     Drop-in replacement for Backbone.Collection. Supports server-side and
+     client-side pagination and sorting. Client-side mode also support fully
+     multi-directional synchronization of changes between pages.
+
+     @class Backbone.PageableCollection
+     @extends Backbone.Collection
+  */
+  var PageableCollection = Backbone.PageableCollection = Backbone.Collection.extend({
+
+    /**
+       The container object to store all pagination states.
+
+       You can override the default state by extending this class or specifying
+       them in an `options` hash to the constructor.
+
+       @property {Object} state
+
+       @property {0|1} [state.firstPage=1] The first page index. Set to 0 if
+       your server API uses 0-based indices. You should only override this value
+       during extension, initialization or reset by the server after
+       fetching. This value should be read only at other times.
+
+       @property {number} [state.lastPage=null] The last page index. This value
+       is __read only__ and it's calculated based on whether `firstPage` is 0 or
+       1, during bootstrapping, fetching and resetting. Please don't change this
+       value under any circumstances.
+
+       @property {number} [state.currentPage=null] The current page index. You
+       should only override this value during extension, initialization or reset
+       by the server after fetching. This value should be read only at other
+       times. Can be a 0-based or 1-based index, depending on whether
+       `firstPage` is 0 or 1. If left as default, it will be set to `firstPage`
+       on initialization.
+
+       @property {number} [state.pageSize=25] How many records to show per
+       page. This value is __read only__ after initialization, if you want to
+       change the page size after initialization, you must call #setPageSize.
+
+       @property {number} [state.totalPages=null] How many pages there are. This
+       value is __read only__ and it is calculated from `totalRecords`.
+
+       @property {number} [state.totalRecords=null] How many records there
+       are. This value is __required__ under server mode. This value is optional
+       for client mode as the number will be the same as the number of models
+       during bootstrapping and during fetching, either supplied by the server
+       in the metadata, or calculated from the size of the response.
+
+       @property {string} [state.sortKey=null] The model attribute to use for
+       sorting.
+
+       @property {-1|0|1} [state.order=-1] The order to use for sorting. Specify
+       -1 for ascending order or 1 for descending order. If 0, no client side
+       sorting will be done and the order query parameter will not be sent to
+       the server during a fetch.
+    */
+    state: {
+      firstPage: 1,
+      lastPage: null,
+      currentPage: null,
+      pageSize: 25,
+      totalPages: null,
+      totalRecords: null,
+      sortKey: null,
+      order: -1
+    },
+
+    /**
+       @property {"server"|"client"|"infinite"} [mode="server"] The mode of
+       operations for this collection. `"server"` paginates on the server-side,
+       `"client"` paginates on the client-side and `"infinite"` paginates on the
+       server-side for APIs that do not support `totalRecords`.
+    */
+    mode: "server",
+
+    /**
+       A translation map to convert Backbone.PageableCollection state attributes
+       to the query parameters accepted by your server API.
+
+       You can override the default state by extending this class or specifying
+       them in `options.queryParams` object hash to the constructor.
+
+       @property {Object} queryParams
+       @property {string} [queryParams.currentPage="page"]
+       @property {string} [queryParams.pageSize="per_page"]
+       @property {string} [queryParams.totalPages="total_pages"]
+       @property {string} [queryParams.totalRecords="total_entries"]
+       @property {string} [queryParams.sortKey="sort_by"]
+       @property {string} [queryParams.order="order"]
+       @property {string} [queryParams.directions={"-1": "asc", "1": "desc"}] A
+       map for translating a Backbone.PageableCollection#state.order constant to
+       the ones your server API accepts.
+    */
+    queryParams: {
+      currentPage: "page",
+      pageSize: "per_page",
+      totalPages: "total_pages",
+      totalRecords: "total_entries",
+      sortKey: "sort_by",
+      order: "order",
+      directions: {
+        "-1": "asc",
+        "1": "desc"
+      }
+    },
+
+    /**
+       __CLIENT MODE ONLY__
+
+       This collection is the internal storage for the bootstrapped or fetched
+       models. You can use this if you want to operate on all the pages.
+
+       @property {Backbone.Collection} fullCollection
+    */
+
+    /**
+       Given a list of models or model attributues, bootstraps the full
+       collection in client mode or infinite mode, or just the page you want in
+       server mode.
+
+       If you want to initialize a collection to a different state than the
+       default, you can specify them in `options.state`. Any state parameters
+       supplied will be merged with the default. If you want to change the
+       default mapping from #state keys to your server API's query parameter
+       names, you can specifiy an object hash in `option.queryParams`. Likewise,
+       any mapping provided will be merged with the default. Lastly, all
+       Backbone.Collection constructor options are also accepted.
+
+       See:
+
+       - Backbone.PageableCollection#state
+       - Backbone.PageableCollection#queryParams
+       - [Backbone.Collection#initialize](http://backbonejs.org/#Collection-constructor)
+
+       @param {Array.<Object>} [models]
+
+       @param {Object} [options]
+
+       @param {function(*, *): number} [options.comparator] If specified, this
+       comparator is set to the current page under server mode, or the #fullCollection
+       otherwise.
+
+       @param {boolean} [options.full] If `false` and either a
+       `options.comparator` or `sortKey` is defined, the comparator is attached
+       to the current page. Default is `true` under client or infinite mode and
+       the comparator will be attached to the #fullCollection.
+
+       @param {Object} [options.state] The state attributes overriding the defaults.
+
+       @param {string} [options.state.sortKey] The model attribute to use for
+       sorting. If specified instead of `options.comparator`, a comparator will
+       be automatically created using this value, and optionally a sorting order
+       specified in `options.state.order`. The comparator is then attached to
+       the new collection instance.
+
+       @param {-1|1} [options.state.order] The order to use for sorting. Specify
+       -1 for ascending order and 1 for descending order.
+
+       @param {Object} [options.queryParam]
+    */
+    constructor: function (models, options) {
+
+      BBColProto.constructor.apply(this, arguments);
+
+      options = options || {};
+
+      var mode = this.mode = options.mode || this.mode || PageableProto.mode;
+
+      var queryParams = _extend({}, PageableProto.queryParams, this.queryParams,
+                                options.queryParams || {});
+
+      queryParams.directions = _extend({},
+                                       PageableProto.queryParams.directions,
+                                       this.queryParams.directions,
+                                       queryParams.directions || {});
+
+      this.queryParams = queryParams;
+
+      var state = this.state = _extend({}, PageableProto.state, this.state,
+                                       options.state || {});
+
+      state.currentPage = state.currentPage == null ?
+        state.firstPage :
+        state.currentPage;
+
+      if (!_isArray(models)) models = models ? [models] : [];
+      models = models.slice();
+
+      if (mode != "server" && state.totalRecords == null && !_isEmpty(models)) {
+        state.totalRecords = models.length;
+      }
+
+      this.switchMode(mode, _extend({fetch: false,
+                                     resetState: false,
+                                     models: models}, options));
+
+      var comparator = options.comparator;
+
+      if (state.sortKey && !comparator) {
+        this.setSorting(state.sortKey, state.order, options);
+      }
+
+      if (mode != "server") {
+        var fullCollection = this.fullCollection;
+
+        if (comparator && options.full) {
+          this.comparator = null;
+          fullCollection.comparator = comparator;
+        }
+
+        if (options.full) fullCollection.sort();
+
+        // make sure the models in the current page and full collection have the
+        // same references
+        if (models && !_isEmpty(models)) {
+          this.reset(models, _extend({silent: true}, options));
+          this.getPage(state.currentPage);
+          models.splice.apply(models, [0, models.length].concat(this.models));
+        }
+      }
+
+      this._initState = _clone(this.state);
+    },
+
+    /**
+       Makes a Backbone.Collection that contains all the pages.
+
+       @private
+       @param {Array.<Object|Backbone.Model>} models
+       @param {Object} options Options for Backbone.Collection constructor.
+       @return {Backbone.Collection}
+    */
+    _makeFullCollection: function (models, options) {
+
+      var properties = ["url", "model", "sync", "comparator"];
+      var thisProto = this.constructor.prototype;
+      var i, length, prop;
+
+      var proto = {};
+      for (i = 0, length = properties.length; i < length; i++) {
+        prop = properties[i];
+        if (!_isUndefined(thisProto[prop])) {
+          proto[prop] = thisProto[prop];
+        }
+      }
+
+      var fullCollection = new (Backbone.Collection.extend(proto))(models, options);
+
+      for (i = 0, length = properties.length; i < length; i++) {
+        prop = properties[i];
+        if (this[prop] !== thisProto[prop]) {
+          fullCollection[prop] = this[prop];
+        }
+      }
+
+      return fullCollection;
+    },
+
+    /**
+       Factory method that returns a Backbone event handler that responses to
+       the `add`, `remove`, `reset`, and the `sort` events. The returned event
+       handler will synchronize the current page collection and the full
+       collection's models.
+
+       @private
+
+       @param {Backbone.PageableCollection} pageCol
+       @param {Backbone.Collection} fullCol
+
+       @return {function(string, Backbone.Model, Backbone.Collection, Object)}
+       Collection event handler
+    */
+    _makeCollectionEventHandler: function (pageCol, fullCol) {
+
+      return function collectionEventHandler (event, model, collection, options) {
+
+        var handlers = pageCol._handlers;
+        _each(_keys(handlers), function (event) {
+          var handler = handlers[event];
+          pageCol.off(event, handler);
+          fullCol.off(event, handler);
+        });
+
+        var state = _clone(pageCol.state);
+        var firstPage = state.firstPage;
+        var currentPage = firstPage === 0 ?
+          state.currentPage :
+          state.currentPage - 1;
+        var pageSize = state.pageSize;
+        var pageStart = currentPage * pageSize, pageEnd = pageStart + pageSize;
+
+        if (event == "add") {
+          var pageIndex, fullIndex, addAt, colToAdd, options = options || {};
+          if (collection == fullCol) {
+            fullIndex = fullCol.indexOf(model);
+            if (fullIndex >= pageStart && fullIndex < pageEnd) {
+              colToAdd = pageCol;
+              pageIndex = addAt = fullIndex - pageStart;
+            }
+          }
+          else {
+            pageIndex = pageCol.indexOf(model);
+            fullIndex = pageStart + pageIndex;
+            colToAdd = fullCol;
+            var addAt = !_isUndefined(options.at) ?
+              options.at + pageStart :
+              fullIndex;
+          }
+
+          if (!options.onRemove) {
+            ++state.totalRecords;
+            delete options.onRemove;
+          }
+
+          pageCol.state = pageCol._checkState(state);
+
+          if (colToAdd) {
+            colToAdd.add(model, _extend({}, options || {}, {at: addAt}));
+            var modelToRemove = pageIndex >= pageSize ?
+              model :
+              !_isUndefined(options.at) && addAt < pageEnd && pageCol.length > pageSize ?
+              pageCol.at(pageSize) :
+              null;
+            if (modelToRemove) {
+              runOnceAtLastHandler(collection, event, function () {
+                pageCol.remove(modelToRemove, {onAdd: true});
+              });
+            }
+          }
+        }
+
+        // remove the model from the other collection as well
+        if (event == "remove") {
+          if (!options.onAdd) {
+            // decrement totalRecords and update totalPages and lastPage
+            if (!--state.totalRecords) {
+              state.totalRecords = null;
+              state.totalPages = null;
+            }
+            else {
+              var totalPages = state.totalPages = ceil(state.totalRecords / pageSize);
+              state.lastPage = firstPage === 0 ? totalPages - 1 : totalPages || firstPage;
+              if (state.currentPage > totalPages) state.currentPage = state.lastPage;
+            }
+            pageCol.state = pageCol._checkState(state);
+
+            var nextModel, removedIndex = options.index;
+            if (collection == pageCol) {
+              if (nextModel = fullCol.at(pageEnd)) {
+                runOnceAtLastHandler(pageCol, event, function () {
+                  pageCol.push(nextModel, {onRemove: true});
+                });
+              }
+              else if (!pageCol.length && state.totalRecords) {
+                pageCol.reset(fullCol.models.slice(pageStart - pageSize, pageEnd - pageSize),
+                              _extend({}, options, {parse: false}));
+              }
+              fullCol.remove(model);
+            }
+            else if (removedIndex >= pageStart && removedIndex < pageEnd) {
+              if (nextModel = fullCol.at(pageEnd - 1)) {
+                runOnceAtLastHandler(pageCol, event, function() {
+                  pageCol.push(nextModel, {onRemove: true});
+                });
+              }
+              pageCol.remove(model);
+              if (!pageCol.length && state.totalRecords) {
+                pageCol.reset(fullCol.models.slice(pageStart - pageSize, pageEnd - pageSize),
+                              _extend({}, options, {parse: false}));
+              }
+            }
+          }
+          else delete options.onAdd;
+        }
+
+        if (event == "reset") {
+          options = collection;
+          collection = model;
+
+          // Reset that's not a result of getPage
+          if (collection == pageCol && options.from == null &&
+              options.to == null) {
+            var head = fullCol.models.slice(0, pageStart);
+            var tail = fullCol.models.slice(pageStart + pageCol.models.length);
+            fullCol.reset(head.concat(pageCol.models).concat(tail), options);
+          }
+          else if (collection == fullCol) {
+            if (!(state.totalRecords = fullCol.models.length)) {
+              state.totalRecords = null;
+              state.totalPages = null;
+            }
+            if (pageCol.mode == "client") {
+              state.lastPage = state.currentPage = state.firstPage;
+            }
+            pageCol.state = pageCol._checkState(state);
+            pageCol.reset(fullCol.models.slice(pageStart, pageEnd),
+                          _extend({}, options, {parse: false}));
+          }
+        }
+
+        if (event == "sort") {
+          options = collection;
+          collection = model;
+          if (collection === fullCol) {
+            pageCol.reset(fullCol.models.slice(pageStart, pageEnd),
+                          _extend({}, options, {parse: false}));
+          }
+        }
+
+        _each(_keys(handlers), function (event) {
+          var handler = handlers[event];
+          _each([pageCol, fullCol], function (col) {
+            col.on(event, handler);
+            var callbacks = col._events[event] || [];
+            callbacks.unshift(callbacks.pop());
+          });
+        });
+      };
+    },
+
+    /**
+       Sanity check this collection's pagination states. Only perform checks
+       when all the required pagination state values are defined and not null.
+       If `totalPages` is undefined or null, it is set to `totalRecords` /
+       `pageSize`. `lastPage` is set according to whether `firstPage` is 0 or 1
+       when no error occurs.
+
+       @private
+
+       @throws {TypeError} If `totalRecords`, `pageSize`, `currentPage` or
+       `firstPage` is not a finite integer.
+
+       @throws {RangeError} If `pageSize`, `currentPage` or `firstPage` is out
+       of bounds.
+
+       @return {Object} Returns the `state` object if no error was found.
+    */
+    _checkState: function (state) {
+
+      var mode = this.mode;
+      var links = this.links;
+      var totalRecords = state.totalRecords;
+      var pageSize = state.pageSize;
+      var currentPage = state.currentPage;
+      var firstPage = state.firstPage;
+      var totalPages = state.totalPages;
+
+      if (totalRecords != null && pageSize != null && currentPage != null &&
+          firstPage != null && (mode == "infinite" ? links : true)) {
+
+        totalRecords = finiteInt(totalRecords, "totalRecords");
+        pageSize = finiteInt(pageSize, "pageSize");
+        currentPage = finiteInt(currentPage, "currentPage");
+        firstPage = finiteInt(firstPage, "firstPage");
+
+        if (pageSize < 1) {
+          throw new RangeError("`pageSize` must be >= 1");
+        }
+
+        totalPages = state.totalPages = ceil(totalRecords / pageSize);
+
+        if (firstPage < 0 || firstPage > 1) {
+          throw new RangeError("`firstPage must be 0 or 1`");
+        }
+
+        state.lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages || firstPage;
+
+        if (mode == "infinite") {
+          if (!links[currentPage + '']) {
+            throw new RangeError("No link found for page " + currentPage);
+          }
+        }
+        else if (currentPage < firstPage ||
+                 (totalPages > 0 &&
+                  (firstPage ? currentPage > totalPages : currentPage >= totalPages))) {
+          throw new RangeError("`currentPage` must be firstPage <= currentPage " +
+                               (firstPage ? ">" : ">=") +
+                               " totalPages if " + firstPage + "-based. Got " +
+                               currentPage + '.');
+        }
+      }
+
+      return state;
+    },
+
+    /**
+       Change the page size of this collection.
+
+       Under most if not all circumstances, you should call this method to
+       change the page size of a pageable collection because it will keep the
+       pagination state sane. By default, the method will recalculate the
+       current page number to one that will retain the current page's models
+       when increasing the page size. When decreasing the page size, this method
+       will retain the last models to the current page that will fit into the
+       smaller page size.
+
+       If `options.first` is true, changing the page size will also reset the
+       current page back to the first page instead of trying to be smart.
+
+       For server mode operations, changing the page size will trigger a #fetch
+       and subsequently a `reset` event.
+
+       For client mode operations, changing the page size will `reset` the
+       current page by recalculating the current page boundary on the client
+       side.
+
+       If `options.fetch` is true, a fetch can be forced if the collection is in
+       client mode.
+
+       @param {number} pageSize The new page size to set to #state.
+       @param {Object} [options] {@link #fetch} options.
+       @param {boolean} [options.first=false] Reset the current page number to
+       the first page if `true`.
+       @param {boolean} [options.fetch] If `true`, force a fetch in client mode.
+
+       @throws {TypeError} If `pageSize` is not a finite integer.
+       @throws {RangeError} If `pageSize` is less than 1.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this.
+    */
+    setPageSize: function (pageSize, options) {
+      pageSize = finiteInt(pageSize, "pageSize");
+
+      options = options || {first: false};
+
+      var state = this.state;
+      var totalPages = ceil(state.totalRecords / pageSize);
+      var currentPage = totalPages ?
+          max(state.firstPage, floor(totalPages * state.currentPage / state.totalPages)) :
+        state.firstPage;
+
+      state = this.state = this._checkState(_extend({}, state, {
+        pageSize: pageSize,
+        currentPage: options.first ? state.firstPage : currentPage,
+        totalPages: totalPages
+      }));
+
+      return this.getPage(state.currentPage, _omit(options, ["first"]));
+    },
+
+    /**
+       Switching between client, server and infinite mode.
+
+       If switching from client to server mode, the #fullCollection is emptied
+       first and then deleted and a fetch is immediately issued for the current
+       page from the server. Pass `false` to `options.fetch` to skip fetching.
+
+       If switching to infinite mode, and if `options.models` is given for an
+       array of models, #links will be populated with a URL per page, using the
+       default URL for this collection.
+
+       If switching from server to client mode, all of the pages are immediately
+       refetched. If you have too many pages, you can pass `false` to
+       `options.fetch` to skip fetching.
+
+       If switching to any mode from infinite mode, the #links will be deleted.
+
+       @param {"server"|"client"|"infinite"} [mode] The mode to switch to.
+
+       @param {Object} [options]
+
+       @param {boolean} [options.fetch=true] If `false`, no fetching is done.
+
+       @param {boolean} [options.resetState=true] If 'false', the state is not
+       reset, but checked for sanity instead.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this if `options.fetch` is `false`.
+    */
+    switchMode: function (mode, options) {
+
+      if (!_contains(["server", "client", "infinite"], mode)) {
+        throw new TypeError('`mode` must be one of "server", "client" or "infinite"');
+      }
+
+      options = options || {fetch: true, resetState: true};
+
+      var state = this.state = options.resetState ?
+        _clone(this._initState) :
+        this._checkState(_extend({}, this.state));
+
+      this.mode = mode;
+
+      var self = this;
+      var fullCollection = this.fullCollection;
+      var handlers = this._handlers = this._handlers || {}, handler;
+      if (mode != "server" && !fullCollection) {
+        fullCollection = this._makeFullCollection(options.models || [], options);
+        fullCollection.pageableCollection = this;
+        this.fullCollection = fullCollection;
+        var allHandler = this._makeCollectionEventHandler(this, fullCollection);
+        _each(["add", "remove", "reset", "sort"], function (event) {
+          handlers[event] = handler = _.bind(allHandler, {}, event);
+          self.on(event, handler);
+          fullCollection.on(event, handler);
+        });
+        fullCollection.comparator = this._fullComparator;
+      }
+      else if (mode == "server" && fullCollection) {
+        _each(_keys(handlers), function (event) {
+          handler = handlers[event];
+          self.off(event, handler);
+          fullCollection.off(event, handler);
+        });
+        delete this._handlers;
+        this._fullComparator = fullCollection.comparator;
+        delete this.fullCollection;
+      }
+
+      if (mode == "infinite") {
+        var links = this.links = {};
+        var firstPage = state.firstPage;
+        var totalPages = ceil(state.totalRecords / state.pageSize);
+        var lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages || firstPage;
+        for (var i = state.firstPage; i <= lastPage; i++) {
+          links[i] = this.url;
+        }
+      }
+      else if (this.links) delete this.links;
+
+      return options.fetch ?
+        this.fetch(_omit(options, "fetch", "resetState")) :
+        this;
+    },
+
+    /**
+       @return {boolean} `true` if this collection can page backward, `false`
+       otherwise.
+    */
+    hasPreviousPage: function () {
+      var state = this.state;
+      var currentPage = state.currentPage;
+      if (this.mode != "infinite") return currentPage > state.firstPage;
+      return !!this.links[currentPage - 1];
+    },
+
+    /**
+       @return {boolean} `true` if this collection can page forward, `false`
+       otherwise.
+    */
+    hasNextPage: function () {
+      var state = this.state;
+      var currentPage = this.state.currentPage;
+      if (this.mode != "infinite") return currentPage < state.lastPage;
+      return !!this.links[currentPage + 1];
+    },
+
+    /**
+       Fetch the first page in server mode, or reset the current page of this
+       collection to the first page in client or infinite mode.
+
+       @param {Object} options {@link #getPage} options.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this.
+    */
+    getFirstPage: function (options) {
+      return this.getPage("first", options);
+    },
+
+    /**
+       Fetch the previous page in server mode, or reset the current page of this
+       collection to the previous page in client or infinite mode.
+
+       @param {Object} options {@link #getPage} options.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this.
+    */
+    getPreviousPage: function (options) {
+      return this.getPage("prev", options);
+    },
+
+    /**
+       Fetch the next page in server mode, or reset the current page of this
+       collection to the next page in client mode.
+
+       @param {Object} options {@link #getPage} options.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this.
+    */
+    getNextPage: function (options) {
+      return this.getPage("next", options);
+    },
+
+    /**
+       Fetch the last page in server mode, or reset the current page of this
+       collection to the last page in client mode.
+
+       @param {Object} options {@link #getPage} options.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this.
+    */
+    getLastPage: function (options) {
+      return this.getPage("last", options);
+    },
+
+    /**
+       Given a page index, set #state.currentPage to that index. If this
+       collection is in server mode, fetch the page using the updated state,
+       otherwise, reset the current page of this collection to the page
+       specified by `index` in client mode. If `options.fetch` is true, a fetch
+       can be forced in client mode before resetting the current page. Under
+       infinite mode, if the index is less than the current page, a reset is
+       done as in client mode. If the index is greater than the current page
+       number, a fetch is made with the results **appended** to #fullCollection.
+       The current page will then be reset after fetching.
+
+       @param {number|string} index The page index to go to, or the page name to
+       look up from #links in infinite mode.
+       @param {Object} [options] {@link #fetch} options or
+       [reset](http://backbonejs.org/#Collection-reset) options for client mode
+       when `options.fetch` is `false`.
+       @param {boolean} [options.fetch=false] If true, force a {@link #fetch} in
+       client mode.
+
+       @throws {TypeError} If `index` is not a finite integer under server or
+       client mode, or does not yield a URL from #links under infinite mode.
+
+       @throws {RangeError} If `index` is out of bounds.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this.
+    */
+    getPage: function (index, options) {
+
+      var mode = this.mode, fullCollection = this.fullCollection;
+
+      options = options || {fetch: false};
+
+      var state = this.state,
+      firstPage = state.firstPage,
+      currentPage = state.currentPage,
+      lastPage = state.lastPage,
+      pageSize = state.pageSize;
+
+      var pageNum = index;
+      switch (index) {
+        case "first": pageNum = firstPage; break;
+        case "prev": pageNum = currentPage - 1; break;
+        case "next": pageNum = currentPage + 1; break;
+        case "last": pageNum = lastPage; break;
+        default: pageNum = finiteInt(index, "index");
+      }
+
+      this.state = this._checkState(_extend({}, state, {currentPage: pageNum}));
+
+      options.from = currentPage, options.to = pageNum;
+
+      var pageStart = (firstPage === 0 ? pageNum : pageNum - 1) * pageSize;
+      var pageModels = fullCollection && fullCollection.length ?
+        fullCollection.models.slice(pageStart, pageStart + pageSize) :
+        [];
+      if ((mode == "client" || (mode == "infinite" && !_isEmpty(pageModels))) &&
+          !options.fetch) {
+        this.reset(pageModels, _omit(options, "fetch"));
+        return this;
+      }
+
+      if (mode == "infinite") options.url = this.links[pageNum];
+
+      return this.fetch(_omit(options, "fetch"));
+    },
+
+    /**
+       Fetch the page for the provided item offset in server mode, or reset the current page of this
+       collection to the page for the provided item offset in client mode.
+
+       @param {Object} options {@link #getPage} options.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this.
+    */
+    getPageByOffset: function (offset, options) {
+      if (offset < 0) {
+        throw new RangeError("`offset must be > 0`");
+      }
+      offset = finiteInt(offset);
+
+      var page = floor(offset / this.state.pageSize);
+      if (this.state.firstPage !== 0) page++;
+      if (page > this.state.lastPage) page = this.state.lastPage;
+      return this.getPage(page, options);
+    },
+
+    /**
+       Overidden to make `getPage` compatible with Zepto.
+
+       @param {string} method
+       @param {Backbone.Model|Backbone.Collection} model
+       @param {Object} [options]
+
+       @return {XMLHttpRequest}
+    */
+    sync: function (method, model, options) {
+      var self = this;
+      if (self.mode == "infinite") {
+        var success = options.success;
+        var currentPage = self.state.currentPage;
+        options.success = function (resp, status, xhr) {
+          var links = self.links;
+          var newLinks = self.parseLinks(resp, _extend({xhr: xhr}, options));
+          if (newLinks.first) links[self.state.firstPage] = newLinks.first;
+          if (newLinks.prev) links[currentPage - 1] = newLinks.prev;
+          if (newLinks.next) links[currentPage + 1] = newLinks.next;
+          if (success) success(resp, status, xhr);
+        };
+      }
+
+      return (BBColProto.sync || Backbone.sync).call(self, method, model, options);
+    },
+
+    /**
+       Parse pagination links from the server response. Only valid under
+       infinite mode.
+
+       Given a response body and a XMLHttpRequest object, extract pagination
+       links from them for infinite paging.
+
+       This default implementation parses the RFC 5988 `Link` header and extract
+       3 links from it - `first`, `prev`, `next`. Any subclasses overriding this
+       method __must__ return an object hash having only the keys
+       above. However, simply returning a `next` link or an empty hash if there
+       are no more links should be enough for most implementations.
+
+       @param {*} resp The deserialized response body.
+       @param {Object} [options]
+       @param {XMLHttpRequest} [options.xhr] The XMLHttpRequest object for this
+       response.
+       @return {Object}
+    */
+    parseLinks: function (resp, options) {
+      var links = {};
+      var linkHeader = options.xhr.getResponseHeader("Link");
+      if (linkHeader) {
+        var relations = ["first", "prev", "next"];
+        _each(linkHeader.split(","), function (linkValue) {
+          var linkParts = linkValue.split(";");
+          var url = linkParts[0].replace(URL_TRIM_RE, '');
+          var params = linkParts.slice(1);
+          _each(params, function (param) {
+            var paramParts = param.split("=");
+            var key = paramParts[0].replace(PARAM_TRIM_RE, '');
+            var value = paramParts[1].replace(PARAM_TRIM_RE, '');
+            if (key == "rel" && _contains(relations, value)) links[value] = url;
+          });
+        });
+      }
+
+      return links;
+    },
+
+    /**
+       Parse server response data.
+
+       This default implementation assumes the response data is in one of two
+       structures:
+
+           [
+             {}, // Your new pagination state
+             [{}, ...] // An array of JSON objects
+           ]
+
+       Or,
+
+           [{}] // An array of JSON objects
+
+       The first structure is the preferred form because the pagination states
+       may have been updated on the server side, sending them down again allows
+       this collection to update its states. If the response has a pagination
+       state object, it is checked for errors.
+
+       The second structure is the
+       [Backbone.Collection#parse](http://backbonejs.org/#Collection-parse)
+       default.
+
+       **Note:** this method has been further simplified since 1.1.7. While
+       existing #parse implementations will continue to work, new code is
+       encouraged to override #parseState and #parseRecords instead.
+
+       @param {Object} resp The deserialized response data from the server.
+       @param {Object} the options for the ajax request
+
+       @return {Array.<Object>} An array of model objects
+    */
+    parse: function (resp, options) {
+      var newState = this.parseState(resp, _clone(this.queryParams), _clone(this.state), options);
+      if (newState) this.state = this._checkState(_extend({}, this.state, newState));
+      return this.parseRecords(resp, options);
+    },
+
+    /**
+       Parse server response for server pagination state updates. Not applicable
+       under infinite mode.
+
+       This default implementation first checks whether the response has any
+       state object as documented in #parse. If it exists, a state object is
+       returned by mapping the server state keys to this pageable collection
+       instance's query parameter keys using `queryParams`.
+
+       It is __NOT__ neccessary to return a full state object complete with all
+       the mappings defined in #queryParams. Any state object resulted is merged
+       with a copy of the current pageable collection state and checked for
+       sanity before actually updating. Most of the time, simply providing a new
+       `totalRecords` value is enough to trigger a full pagination state
+       recalculation.
+
+           parseState: function (resp, queryParams, state, options) {
+             return {totalRecords: resp.total_entries};
+           }
+
+       If you want to use header fields use:
+
+           parseState: function (resp, queryParams, state, options) {
+               return {totalRecords: options.xhr.getResponseHeader("X-total")};
+           }
+
+       This method __MUST__ return a new state object instead of directly
+       modifying the #state object. The behavior of directly modifying #state is
+       undefined.
+
+       @param {Object} resp The deserialized response data from the server.
+       @param {Object} queryParams A copy of #queryParams.
+       @param {Object} state A copy of #state.
+       @param {Object} [options] The options passed through from
+       `parse`. (backbone >= 0.9.10 only)
+
+       @return {Object} A new (partial) state object.
+     */
+    parseState: function (resp, queryParams, state, options) {
+      if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) {
+
+        var newState = _clone(state);
+        var serverState = resp[0];
+
+        _each(_pairs(_omit(queryParams, "directions")), function (kvp) {
+          var k = kvp[0], v = kvp[1];
+          var serverVal = serverState[v];
+          if (!_isUndefined(serverVal) && !_.isNull(serverVal)) newState[k] = serverState[v];
+        });
+
+        if (serverState.order) {
+          newState.order = _invert(queryParams.directions)[serverState.order] * 1;
+        }
+
+        return newState;
+      }
+    },
+
+    /**
+       Parse server response for an array of model objects.
+
+       This default implementation first checks whether the response has any
+       state object as documented in #parse. If it exists, the array of model
+       objects is assumed to be the second element, otherwise the entire
+       response is returned directly.
+
+       @param {Object} resp The deserialized response data from the server.
+       @param {Object} [options] The options passed through from the
+       `parse`. (backbone >= 0.9.10 only)
+
+       @return {Array.<Object>} An array of model objects
+     */
+    parseRecords: function (resp, options) {
+      if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) {
+        return resp[1];
+      }
+
+      return resp;
+    },
+
+    /**
+       Fetch a page from the server in server mode, or all the pages in client
+       mode. Under infinite mode, the current page is refetched by default and
+       then reset.
+
+       The query string is constructed by translating the current pagination
+       state to your server API query parameter using #queryParams. The current
+       page will reset after fetch.
+
+       @param {Object} [options] Accepts all
+       [Backbone.Collection#fetch](http://backbonejs.org/#Collection-fetch)
+       options.
+
+       @return {XMLHttpRequest}
+    */
+    fetch: function (options) {
+
+      options = options || {};
+
+      var state = this._checkState(this.state);
+
+      var mode = this.mode;
+
+      if (mode == "infinite" && !options.url) {
+        options.url = this.links[state.currentPage];
+      }
+
+      var data = options.data || {};
+
+      // dedup query params
+      var url = options.url || this.url || "";
+      if (_isFunction(url)) url = url.call(this);
+      var qsi = url.indexOf('?');
+      if (qsi != -1) {
+        _extend(data, queryStringToParams(url.slice(qsi + 1)));
+        url = url.slice(0, qsi);
+      }
+
+      options.url = url;
+      options.data = data;
+
+      // map params except directions
+      var queryParams = this.mode == "client" ?
+        _pick(this.queryParams, "sortKey", "order") :
+        _omit(_pick(this.queryParams, _keys(PageableProto.queryParams)),
+              "directions");
+
+      var i, kvp, k, v, kvps = _pairs(queryParams), thisCopy = _clone(this);
+      for (i = 0; i < kvps.length; i++) {
+        kvp = kvps[i], k = kvp[0], v = kvp[1];
+        v = _isFunction(v) ? v.call(thisCopy) : v;
+        if (state[k] != null && v != null) {
+          data[v] = state[k];
+        }
+      }
+
+      // fix up sorting parameters
+      if (state.sortKey && state.order) {
+        var o = _isFunction(queryParams.order) ?
+          queryParams.order.call(thisCopy) :
+          queryParams.order;
+        data[o] = this.queryParams.directions[state.order + ""];
+      }
+      else if (!state.sortKey) delete data[queryParams.order];
+
+      // map extra query parameters
+      var extraKvps = _pairs(_omit(this.queryParams,
+                                   _keys(PageableProto.queryParams)));
+      for (i = 0; i < extraKvps.length; i++) {
+        kvp = extraKvps[i];
+        v = kvp[1];
+        v = _isFunction(v) ? v.call(thisCopy) : v;
+        if (v != null) data[kvp[0]] = v;
+      }
+
+      if (mode != "server") {
+        var self = this, fullCol = this.fullCollection;
+        var success = options.success;
+        options.success = function (col, resp, opts) {
+
+          // make sure the caller's intent is obeyed
+          opts = opts || {};
+          if (_isUndefined(options.silent)) delete opts.silent;
+          else opts.silent = options.silent;
+
+          var models = col.models;
+          if (mode == "client") fullCol.reset(models, opts);
+          else {
+            fullCol.add(models, _extend({at: fullCol.length},
+                                        _extend(opts, {parse: false})));
+            self.trigger("reset", self, opts);
+          }
+
+          if (success) success(col, resp, opts);
+        };
+
+        // silent the first reset from backbone
+        return BBColProto.fetch.call(this, _extend({}, options, {silent: true}));
+      }
+
+      return BBColProto.fetch.call(this, options);
+    },
+
+    /**
+       Convenient method for making a `comparator` sorted by a model attribute
+       identified by `sortKey` and ordered by `order`.
+
+       Like a Backbone.Collection, a Backbone.PageableCollection will maintain
+       the __current page__ in sorted order on the client side if a `comparator`
+       is attached to it. If the collection is in client mode, you can attach a
+       comparator to #fullCollection to have all the pages reflect the global
+       sorting order by specifying an option `full` to `true`. You __must__ call
+       `sort` manually or #fullCollection.sort after calling this method to
+       force a resort.
+
+       While you can use this method to sort the current page in server mode,
+       the sorting order may not reflect the global sorting order due to the
+       additions or removals of the records on the server since the last
+       fetch. If you want the most updated page in a global sorting order, it is
+       recommended that you set #state.sortKey and optionally #state.order, and
+       then call #fetch.
+
+       @protected
+
+       @param {string} [sortKey=this.state.sortKey] See `state.sortKey`.
+       @param {number} [order=this.state.order] See `state.order`.
+       @param {(function(Backbone.Model, string): Object) | string} [sortValue] See #setSorting.
+
+       See [Backbone.Collection.comparator](http://backbonejs.org/#Collection-comparator).
+    */
+    _makeComparator: function (sortKey, order, sortValue) {
+      var state = this.state;
+
+      sortKey = sortKey || state.sortKey;
+      order = order || state.order;
+
+      if (!sortKey || !order) return;
+
+      if (!sortValue) sortValue = function (model, attr) {
+        return model.get(attr);
+      };
+
+      return function (left, right) {
+        var l = sortValue(left, sortKey), r = sortValue(right, sortKey), t;
+        if (order === 1) t = l, l = r, r = t;
+        if (l === r) return 0;
+        else if (l < r) return -1;
+        return 1;
+      };
+    },
+
+    /**
+       Adjusts the sorting for this pageable collection.
+
+       Given a `sortKey` and an `order`, sets `state.sortKey` and
+       `state.order`. A comparator can be applied on the client side to sort in
+       the order defined if `options.side` is `"client"`. By default the
+       comparator is applied to the #fullCollection. Set `options.full` to
+       `false` to apply a comparator to the current page under any mode. Setting
+       `sortKey` to `null` removes the comparator from both the current page and
+       the full collection.
+
+       If a `sortValue` function is given, it will be passed the `(model,
+       sortKey)` arguments and is used to extract a value from the model during
+       comparison sorts. If `sortValue` is not given, `model.get(sortKey)` is
+       used for sorting.
+
+       @chainable
+
+       @param {string} sortKey See `state.sortKey`.
+       @param {number} [order=this.state.order] See `state.order`.
+       @param {Object} [options]
+       @param {"server"|"client"} [options.side] By default, `"client"` if
+       `mode` is `"client"`, `"server"` otherwise.
+       @param {boolean} [options.full=true]
+       @param {(function(Backbone.Model, string): Object) | string} [options.sortValue]
+    */
+    setSorting: function (sortKey, order, options) {
+
+      var state = this.state;
+
+      state.sortKey = sortKey;
+      state.order = order = order || state.order;
+
+      var fullCollection = this.fullCollection;
+
+      var delComp = false, delFullComp = false;
+
+      if (!sortKey) delComp = delFullComp = true;
+
+      var mode = this.mode;
+      options = _extend({side: mode == "client" ? mode : "server", full: true},
+                        options);
+
+      var comparator = this._makeComparator(sortKey, order, options.sortValue);
+
+      var full = options.full, side = options.side;
+
+      if (side == "client") {
+        if (full) {
+          if (fullCollection) fullCollection.comparator = comparator;
+          delComp = true;
+        }
+        else {
+          this.comparator = comparator;
+          delFullComp = true;
+        }
+      }
+      else if (side == "server" && !full) {
+        this.comparator = comparator;
+      }
+
+      if (delComp) this.comparator = null;
+      if (delFullComp && fullCollection) fullCollection.comparator = null;
+
+      return this;
+    }
+
+  });
+
+  var PageableProto = PageableCollection.prototype;
+
+  return PageableCollection;
+
+}));
diff --git a/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.min.js b/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.min.js
deleted file mode 100644
index 687349c..0000000
--- a/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.min.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
-  backbone.paginator 2.0.0
-  http://github.com/backbone-paginator/backbone.paginator
-
-  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
-  Licensed under the MIT @license.
-*/
-!function(a){if("object"==typeof exports)module.exports=a(require("underscore"),require("backbone"));else if("function"==typeof define&&define.amd)define(["underscore","backbone"],a);else if("undefined"!=typeof _&&"undefined"!=typeof Backbone){var b=Backbone.PageableCollection,c=a(_,Backbone);Backbone.PageableCollection.noConflict=function(){return Backbone.PageableCollection=b,c}}}(function(a,b){"use strict";function c(b,c){if(!a.isNumber(b)||a.isNaN(b)||!a.isFinite(b)||~~b!==b)throw new TypeError("`"+c+"` must be a finite integer");return b}function d(a){for(var b,c,d,e,f={},g=decodeURIComponent,h=a.split("&"),i=0,j=h.length;j>i;i++){var k=h[i];b=k.split("="),c=b[0],d=b[1]||!0,c=g(c),d=g(d),e=f[c],o(e)?e.push(d):f[c]=e?[e,d]:d}return f}function e(a,b,c){var d=a._events[b];if(d&&d.length){var e=d[d.length-1],f=e.callback;e.callback=function(){try{f.apply(this,arguments),c()}catch(a){throw a}finally{e.callback=f}}}else c()}var f=a.extend,g=a.omit,h=a.clone,i=a.each,j=a.pick,k=a.contains,l=a.isEmpty,m=a.pairs,n=a.invert,o=a.isArray,p=a.isFunction,q=a.isObject,r=a.keys,s=a.isUndefined,t=Math.ceil,u=Math.floor,v=Math.max,w=b.Collection.prototype,x=/[\s'"]/g,y=/[<>\s'"]/g,z=b.PageableCollection=b.Collection.extend({state:{firstPage:1,lastPage:null,currentPage:null,pageSize:25,totalPages:null,totalRecords:null,sortKey:null,order:-1},mode:"server",queryParams:{currentPage:"page",pageSize:"per_page",totalPages:"total_pages",totalRecords:"total_entries",sortKey:"sort_by",order:"order",directions:{"-1":"asc",1:"desc"}},constructor:function(a,b){w.constructor.apply(this,arguments),b=b||{};var c=this.mode=b.mode||this.mode||A.mode,d=f({},A.queryParams,this.queryParams,b.queryParams||{});d.directions=f({},A.queryParams.directions,this.queryParams.directions,d.directions||{}),this.queryParams=d;var e=this.state=f({},A.state,this.state,b.state||{});e.currentPage=null==e.currentPage?e.firstPage:e.currentPage,o(a)||(a=a?[a]:[]),a=a.slice(),"server"==c||null!=e.totalRecords||l(a)||(e.totalRecords=a.length),this.switchMode(c,f({fetch:!1,resetState:!1,models:a},b));var g=b.comparator;if(e.sortKey&&!g&&this.setSorting(e.sortKey,e.order,b),"server"!=c){var i=this.fullCollection;g&&b.full&&(this.comparator=null,i.comparator=g),b.full&&i.sort(),a&&!l(a)&&(this.reset(a,f({silent:!0},b)),this.getPage(e.currentPage),a.splice.apply(a,[0,a.length].concat(this.models)))}this._initState=h(this.state)},_makeFullCollection:function(a,c){var d,e,f,g=["url","model","sync","comparator"],h=this.constructor.prototype,i={};for(d=0,e=g.length;e>d;d++)f=g[d],s(h[f])||(i[f]=h[f]);var j=new(b.Collection.extend(i))(a,c);for(d=0,e=g.length;e>d;d++)f=g[d],this[f]!==h[f]&&(j[f]=this[f]);return j},_makeCollectionEventHandler:function(a,b){return function(c,d,g,j){var k=a._handlers;i(r(k),function(c){var d=k[c];a.off(c,d),b.off(c,d)});var l=h(a.state),m=l.firstPage,n=0===m?l.currentPage:l.currentPage-1,o=l.pageSize,p=n*o,q=p+o;if("add"==c){var u,v,w,x,j=j||{};if(g==b)v=b.indexOf(d),v>=p&&q>v&&(x=a,u=w=v-p);else{u=a.indexOf(d),v=p+u,x=b;var w=s(j.at)?v:j.at+p}if(j.onRemove||(++l.totalRecords,delete j.onRemove),a.state=a._checkState(l),x){x.add(d,f({},j||{},{at:w}));var y=u>=o?d:!s(j.at)&&q>w&&a.length>o?a.at(o):null;y&&e(g,c,function(){a.remove(y,{onAdd:!0})})}}if("remove"==c)if(j.onAdd)delete j.onAdd;else{if(--l.totalRecords){var z=l.totalPages=t(l.totalRecords/o);l.lastPage=0===m?z-1:z||m,l.currentPage>z&&(l.currentPage=l.lastPage)}else l.totalRecords=null,l.totalPages=null;a.state=a._checkState(l);var A,B=j.index;g==a?((A=b.at(q))?e(a,c,function(){a.push(A,{onRemove:!0})}):!a.length&&l.totalRecords&&a.reset(b.models.slice(p-o,q-o),f({},j,{parse:!1})),b.remove(d)):B>=p&&q>B&&((A=b.at(q-1))&&e(a,c,function(){a.push(A,{onRemove:!0})}),a.remove(d),!a.length&&l.totalRecords&&a.reset(b.models.slice(p-o,q-o),f({},j,{parse:!1})))}if("reset"==c)if(j=g,g=d,g==a&&null==j.from&&null==j.to){var C=b.models.slice(0,p),D=b.models.slice(p+a.models.length);b.reset(C.concat(a.models).concat(D),j)}else g==b&&((l.totalRecords=b.models.length)||(l.totalRecords=null,l.totalPages=null),"client"==a.mode&&(l.lastPage=l.currentPage=l.firstPage),a.state=a._checkState(l),a.reset(b.models.slice(p,q),f({},j,{parse:!1})));"sort"==c&&(j=g,g=d,g===b&&a.reset(b.models.slice(p,q),f({},j,{parse:!1}))),i(r(k),function(c){var d=k[c];i([a,b],function(a){a.on(c,d);var b=a._events[c]||[];b.unshift(b.pop())})})}},_checkState:function(a){var b=this.mode,d=this.links,e=a.totalRecords,f=a.pageSize,g=a.currentPage,h=a.firstPage,i=a.totalPages;if(null!=e&&null!=f&&null!=g&&null!=h&&("infinite"==b?d:!0)){if(e=c(e,"totalRecords"),f=c(f,"pageSize"),g=c(g,"currentPage"),h=c(h,"firstPage"),1>f)throw new RangeError("`pageSize` must be >= 1");if(i=a.totalPages=t(e/f),0>h||h>1)throw new RangeError("`firstPage must be 0 or 1`");if(a.lastPage=0===h?v(0,i-1):i||h,"infinite"==b){if(!d[g+""])throw new RangeError("No link found for page "+g)}else if(h>g||i>0&&(h?g>i:g>=i))throw new RangeError("`currentPage` must be firstPage <= currentPage "+(h?">":">=")+" totalPages if "+h+"-based. Got "+g+".")}return a},setPageSize:function(a,b){a=c(a,"pageSize"),b=b||{first:!1};var d=this.state,e=t(d.totalRecords/a),h=e?v(d.firstPage,u(e*d.currentPage/d.totalPages)):d.firstPage;return d=this.state=this._checkState(f({},d,{pageSize:a,currentPage:b.first?d.firstPage:h,totalPages:e})),this.getPage(d.currentPage,g(b,["first"]))},switchMode:function(b,c){if(!k(["server","client","infinite"],b))throw new TypeError('`mode` must be one of "server", "client" or "infinite"');c=c||{fetch:!0,resetState:!0};var d=this.state=c.resetState?h(this._initState):this._checkState(f({},this.state));this.mode=b;var e,j=this,l=this.fullCollection,m=this._handlers=this._handlers||{};if("server"==b||l)"server"==b&&l&&(i(r(m),function(a){e=m[a],j.off(a,e),l.off(a,e)}),delete this._handlers,this._fullComparator=l.comparator,delete this.fullCollection);else{l=this._makeFullCollection(c.models||[],c),l.pageableCollection=this,this.fullCollection=l;var n=this._makeCollectionEventHandler(this,l);i(["add","remove","reset","sort"],function(b){m[b]=e=a.bind(n,{},b),j.on(b,e),l.on(b,e)}),l.comparator=this._fullComparator}if("infinite"==b)for(var o=this.links={},p=d.firstPage,q=t(d.totalRecords/d.pageSize),s=0===p?v(0,q-1):q||p,u=d.firstPage;s>=u;u++)o[u]=this.url;else this.links&&delete this.links;return c.fetch?this.fetch(g(c,"fetch","resetState")):this},hasPreviousPage:function(){var a=this.state,b=a.currentPage;return"infinite"!=this.mode?b>a.firstPage:!!this.links[b-1]},hasNextPage:function(){var a=this.state,b=this.state.currentPage;return"infinite"!=this.mode?b<a.lastPage:!!this.links[b+1]},getFirstPage:function(a){return this.getPage("first",a)},getPreviousPage:function(a){return this.getPage("prev",a)},getNextPage:function(a){return this.getPage("next",a)},getLastPage:function(a){return this.getPage("last",a)},getPage:function(a,b){var d=this.mode,e=this.fullCollection;b=b||{fetch:!1};var h=this.state,i=h.firstPage,j=h.currentPage,k=h.lastPage,m=h.pageSize,n=a;switch(a){case"first":n=i;break;case"prev":n=j-1;break;case"next":n=j+1;break;case"last":n=k;break;default:n=c(a,"index")}this.state=this._checkState(f({},h,{currentPage:n})),b.from=j,b.to=n;var o=(0===i?n:n-1)*m,p=e&&e.length?e.models.slice(o,o+m):[];return"client"!=d&&("infinite"!=d||l(p))||b.fetch?("infinite"==d&&(b.url=this.links[n]),this.fetch(g(b,"fetch"))):(this.reset(p,g(b,"fetch")),this)},getPageByOffset:function(a,b){if(0>a)throw new RangeError("`offset must be > 0`");a=c(a);var d=u(a/this.state.pageSize);return 0!==this.state.firstPage&&d++,d>this.state.lastPage&&(d=this.state.lastPage),this.getPage(d,b)},sync:function(a,c,d){var e=this;if("infinite"==e.mode){var g=d.success,h=e.state.currentPage;d.success=function(a,b,c){var i=e.links,j=e.parseLinks(a,f({xhr:c},d));j.first&&(i[e.state.firstPage]=j.first),j.prev&&(i[h-1]=j.prev),j.next&&(i[h+1]=j.next),g&&g(a,b,c)}}return(w.sync||b.sync).call(e,a,c,d)},parseLinks:function(a,b){var c={},d=b.xhr.getResponseHeader("Link");if(d){var e=["first","prev","next"];i(d.split(","),function(a){var b=a.split(";"),d=b[0].replace(y,""),f=b.slice(1);i(f,function(a){var b=a.split("="),f=b[0].replace(x,""),g=b[1].replace(x,"");"rel"==f&&k(e,g)&&(c[g]=d)})})}return c},parse:function(a,b){var c=this.parseState(a,h(this.queryParams),h(this.state),b);return c&&(this.state=this._checkState(f({},this.state,c))),this.parseRecords(a,b)},parseState:function(b,c,d){if(b&&2===b.length&&q(b[0])&&o(b[1])){var e=h(d),f=b[0];return i(m(g(c,"directions")),function(b){var c=b[0],d=b[1],g=f[d];s(g)||a.isNull(g)||(e[c]=f[d])}),f.order&&(e.order=1*n(c.directions)[f.order]),e}},parseRecords:function(a){return a&&2===a.length&&q(a[0])&&o(a[1])?a[1]:a},fetch:function(a){a=a||{};var b=this._checkState(this.state),c=this.mode;"infinite"!=c||a.url||(a.url=this.links[b.currentPage]);var e=a.data||{},i=a.url||this.url||"";p(i)&&(i=i.call(this));var k=i.indexOf("?");-1!=k&&(f(e,d(i.slice(k+1))),i=i.slice(0,k)),a.url=i,a.data=e;var l,n,o,q,t="client"==this.mode?j(this.queryParams,"sortKey","order"):g(j(this.queryParams,r(A.queryParams)),"directions"),u=m(t),v=h(this);for(l=0;l<u.length;l++)n=u[l],o=n[0],q=n[1],q=p(q)?q.call(v):q,null!=b[o]&&null!=q&&(e[q]=b[o]);if(b.sortKey&&b.order){var x=p(t.order)?t.order.call(v):t.order;e[x]=this.queryParams.directions[b.order+""]}else b.sortKey||delete e[t.order];var y=m(g(this.queryParams,r(A.queryParams)));for(l=0;l<y.length;l++)n=y[l],q=n[1],q=p(q)?q.call(v):q,null!=q&&(e[n[0]]=q);if("server"!=c){var z=this,B=this.fullCollection,C=a.success;return a.success=function(b,d,e){e=e||{},s(a.silent)?delete e.silent:e.silent=a.silent;var g=b.models;"client"==c?B.reset(g,e):(B.add(g,f({at:B.length},f(e,{parse:!1}))),z.trigger("reset",z,e)),C&&C(b,d,e)},w.fetch.call(this,f({},a,{silent:!0}))}return w.fetch.call(this,a)},_makeComparator:function(a,b,c){var d=this.state;return a=a||d.sortKey,b=b||d.order,a&&b?(c||(c=function(a,b){return a.get(b)}),function(d,e){var f,g=c(d,a),h=c(e,a);return 1===b&&(f=g,g=h,h=f),g===h?0:h>g?-1:1}):void 0},setSorting:function(a,b,c){var d=this.state;d.sortKey=a,d.order=b=b||d.order;var e=this.fullCollection,g=!1,h=!1;a||(g=h=!0);var i=this.mode;c=f({side:"client"==i?i:"server",full:!0},c);var j=this._makeComparator(a,b,c.sortValue),k=c.full,l=c.side;return"client"==l?k?(e&&(e.comparator=j),g=!0):(this.comparator=j,h=!0):"server"!=l||k||(this.comparator=j),g&&(this.comparator=null),h&&e&&(e.comparator=null),this}}),A=z.prototype;return z});
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/backgrid-0.3.5.js b/htrace-core/src/web/lib/js/backgrid-0.3.5.js
new file mode 100644
index 0000000..6f8da79
--- /dev/null
+++ b/htrace-core/src/web/lib/js/backgrid-0.3.5.js
@@ -0,0 +1,2883 @@
+/*!
+  backgrid
+  http://github.com/wyuenho/backgrid
+
+  Copyright (c) 2014 Jimmy Yuen Ho Wong and contributors <wyuenho@gmail.com>
+  Licensed under the MIT license.
+*/
+
+(function (factory) {
+
+  // CommonJS
+  if (typeof exports == "object") {
+    module.exports = factory(module.exports,
+                             require("underscore"),
+                             require("backbone"));
+  }
+  // Browser
+  else factory(this, this._, this.Backbone);
+}(function (root, _, Backbone) {
+
+  "use strict";
+
+/*
+  backgrid
+  http://github.com/wyuenho/backgrid
+
+  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+  Licensed under the MIT license.
+*/
+
+// Copyright 2009, 2010 Kristopher Michael Kowal
+// https://github.com/kriskowal/es5-shim
+// ES5 15.5.4.20
+// http://es5.github.com/#x15.5.4.20
+var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" +
+  "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" +
+  "\u2029\uFEFF";
+if (!String.prototype.trim || ws.trim()) {
+  // http://blog.stevenlevithan.com/archives/faster-trim-javascript
+  // http://perfectionkills.com/whitespace-deviations/
+  ws = "[" + ws + "]";
+  var trimBeginRegexp = new RegExp("^" + ws + ws + "*"),
+  trimEndRegexp = new RegExp(ws + ws + "*$");
+  String.prototype.trim = function trim() {
+    if (this === undefined || this === null) {
+      throw new TypeError("can't convert " + this + " to object");
+    }
+    return String(this)
+      .replace(trimBeginRegexp, "")
+      .replace(trimEndRegexp, "");
+  };
+}
+
+function lpad(str, length, padstr) {
+  var paddingLen = length - (str + '').length;
+  paddingLen =  paddingLen < 0 ? 0 : paddingLen;
+  var padding = '';
+  for (var i = 0; i < paddingLen; i++) {
+    padding = padding + padstr;
+  }
+  return padding + str;
+}
+
+var $ = Backbone.$;
+
+var Backgrid = root.Backgrid = {
+
+  Extension: {},
+
+  resolveNameToClass: function (name, suffix) {
+    if (_.isString(name)) {
+      var key = _.map(name.split('-'), function (e) {
+        return e.slice(0, 1).toUpperCase() + e.slice(1);
+      }).join('') + suffix;
+      var klass = Backgrid[key] || Backgrid.Extension[key];
+      if (_.isUndefined(klass)) {
+        throw new ReferenceError("Class '" + key + "' not found");
+      }
+      return klass;
+    }
+
+    return name;
+  },
+
+  callByNeed: function () {
+    var value = arguments[0];
+    if (!_.isFunction(value)) return value;
+
+    var context = arguments[1];
+    var args = [].slice.call(arguments, 2);
+    return value.apply(context, !!(args + '') ? args : []);
+  }
+
+};
+_.extend(Backgrid, Backbone.Events);
+
+/**
+   Command translates a DOM Event into commands that Backgrid
+   recognizes. Interested parties can listen on selected Backgrid events that
+   come with an instance of this class and act on the commands.
+
+   It is also possible to globally rebind the keyboard shortcuts by replacing
+   the methods in this class' prototype.
+
+   @class Backgrid.Command
+   @constructor
+ */
+var Command = Backgrid.Command = function (evt) {
+  _.extend(this, {
+    altKey: !!evt.altKey,
+    "char": evt["char"],
+    charCode: evt.charCode,
+    ctrlKey: !!evt.ctrlKey,
+    key: evt.key,
+    keyCode: evt.keyCode,
+    locale: evt.locale,
+    location: evt.location,
+    metaKey: !!evt.metaKey,
+    repeat: !!evt.repeat,
+    shiftKey: !!evt.shiftKey,
+    which: evt.which
+  });
+};
+_.extend(Command.prototype, {
+  /**
+     Up Arrow
+
+     @member Backgrid.Command
+   */
+  moveUp: function () { return this.keyCode == 38; },
+  /**
+     Down Arrow
+
+     @member Backgrid.Command
+   */
+  moveDown: function () { return this.keyCode === 40; },
+  /**
+     Shift Tab
+
+     @member Backgrid.Command
+   */
+  moveLeft: function () { return this.shiftKey && this.keyCode === 9; },
+  /**
+     Tab
+
+     @member Backgrid.Command
+   */
+  moveRight: function () { return !this.shiftKey && this.keyCode === 9; },
+  /**
+     Enter
+
+     @member Backgrid.Command
+   */
+  save: function () { return this.keyCode === 13; },
+  /**
+     Esc
+
+     @member Backgrid.Command
+   */
+  cancel: function () { return this.keyCode === 27; },
+  /**
+     None of the above.
+
+     @member Backgrid.Command
+   */
+  passThru: function () {
+    return !(this.moveUp() || this.moveDown() || this.moveLeft() ||
+             this.moveRight() || this.save() || this.cancel());
+  }
+});
+
+/*
+  backgrid
+  http://github.com/wyuenho/backgrid
+
+  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+  Licensed under the MIT license.
+*/
+
+/**
+   Just a convenient class for interested parties to subclass.
+
+   The default Cell classes don't require the formatter to be a subclass of
+   Formatter as long as the fromRaw(rawData) and toRaw(formattedData) methods
+   are defined.
+
+   @abstract
+   @class Backgrid.CellFormatter
+   @constructor
+*/
+var CellFormatter = Backgrid.CellFormatter = function () {};
+_.extend(CellFormatter.prototype, {
+
+  /**
+     Takes a raw value from a model and returns an optionally formatted string
+     for display. The default implementation simply returns the supplied value
+     as is without any type conversion.
+
+     @member Backgrid.CellFormatter
+     @param {*} rawData
+     @param {Backbone.Model} model Used for more complicated formatting
+     @return {*}
+  */
+  fromRaw: function (rawData, model) {
+    return rawData;
+  },
+
+  /**
+     Takes a formatted string, usually from user input, and returns a
+     appropriately typed value for persistence in the model.
+
+     If the user input is invalid or unable to be converted to a raw value
+     suitable for persistence in the model, toRaw must return `undefined`.
+
+     @member Backgrid.CellFormatter
+     @param {string} formattedData
+     @param {Backbone.Model} model Used for more complicated formatting
+     @return {*|undefined}
+  */
+  toRaw: function (formattedData, model) {
+    return formattedData;
+  }
+
+});
+
+/**
+   A floating point number formatter. Doesn't understand scientific notation at
+   the moment.
+
+   @class Backgrid.NumberFormatter
+   @extends Backgrid.CellFormatter
+   @constructor
+   @throws {RangeError} If decimals < 0 or > 20.
+*/
+var NumberFormatter = Backgrid.NumberFormatter = function (options) {
+  _.extend(this, this.defaults, options || {});
+
+  if (this.decimals < 0 || this.decimals > 20) {
+    throw new RangeError("decimals must be between 0 and 20");
+  }
+};
+NumberFormatter.prototype = new CellFormatter();
+_.extend(NumberFormatter.prototype, {
+
+  /**
+     @member Backgrid.NumberFormatter
+     @cfg {Object} options
+
+     @cfg {number} [options.decimals=2] Number of decimals to display. Must be an integer.
+
+     @cfg {string} [options.decimalSeparator='.'] The separator to use when
+     displaying decimals.
+
+     @cfg {string} [options.orderSeparator=','] The separator to use to
+     separator thousands. May be an empty string.
+   */
+  defaults: {
+    decimals: 2,
+    decimalSeparator: '.',
+    orderSeparator: ','
+  },
+
+  HUMANIZED_NUM_RE: /(\d)(?=(?:\d{3})+$)/g,
+
+  /**
+     Takes a floating point number and convert it to a formatted string where
+     every thousand is separated by `orderSeparator`, with a `decimal` number of
+     decimals separated by `decimalSeparator`. The number returned is rounded
+     the usual way.
+
+     @member Backgrid.NumberFormatter
+     @param {number} number
+     @param {Backbone.Model} model Used for more complicated formatting
+     @return {string}
+  */
+  fromRaw: function (number, model) {
+    if (_.isNull(number) || _.isUndefined(number)) return '';
+
+    number = number.toFixed(~~this.decimals);
+
+    var parts = number.split('.');
+    var integerPart = parts[0];
+    var decimalPart = parts[1] ? (this.decimalSeparator || '.') + parts[1] : '';
+
+    return integerPart.replace(this.HUMANIZED_NUM_RE, '$1' + this.orderSeparator) + decimalPart;
+  },
+
+  /**
+     Takes a string, possibly formatted with `orderSeparator` and/or
+     `decimalSeparator`, and convert it back to a number.
+
+     @member Backgrid.NumberFormatter
+     @param {string} formattedData
+     @param {Backbone.Model} model Used for more complicated formatting
+     @return {number|undefined} Undefined if the string cannot be converted to
+     a number.
+  */
+  toRaw: function (formattedData, model) {
+    formattedData = formattedData.trim();
+
+    if (formattedData === '') return null;
+
+    var rawData = '';
+
+    var thousands = formattedData.split(this.orderSeparator);
+    for (var i = 0; i < thousands.length; i++) {
+      rawData += thousands[i];
+    }
+
+    var decimalParts = rawData.split(this.decimalSeparator);
+    rawData = '';
+    for (var i = 0; i < decimalParts.length; i++) {
+      rawData = rawData + decimalParts[i] + '.';
+    }
+
+    if (rawData[rawData.length - 1] === '.') {
+      rawData = rawData.slice(0, rawData.length - 1);
+    }
+
+    var result = (rawData * 1).toFixed(~~this.decimals) * 1;
+    if (_.isNumber(result) && !_.isNaN(result)) return result;
+  }
+
+});
+
+/**
+   A number formatter that converts a floating point number, optionally
+   multiplied by a multiplier, to a percentage string and vice versa.
+
+   @class Backgrid.PercentFormatter
+   @extends Backgrid.NumberFormatter
+   @constructor
+   @throws {RangeError} If decimals < 0 or > 20.
+ */
+var PercentFormatter = Backgrid.PercentFormatter = function () {
+  Backgrid.NumberFormatter.apply(this, arguments);
+};
+
+PercentFormatter.prototype = new Backgrid.NumberFormatter(),
+
+_.extend(PercentFormatter.prototype, {
+
+  /**
+     @member Backgrid.PercentFormatter
+     @cfg {Object} options
+
+     @cfg {number} [options.multiplier=1] The number used to multiply the model
+     value for display.
+
+     @cfg {string} [options.symbol='%'] The symbol to append to the percentage
+     string.
+   */
+  defaults: _.extend({}, NumberFormatter.prototype.defaults, {
+    multiplier: 1,
+    symbol: "%"
+  }),
+
+  /**
+     Takes a floating point number, where the number is first multiplied by
+     `multiplier`, then converted to a formatted string like
+     NumberFormatter#fromRaw, then finally append `symbol` to the end.
+
+     @member Backgrid.PercentFormatter
+     @param {number} rawValue
+     @param {Backbone.Model} model Used for more complicated formatting
+     @return {string}
+  */
+  fromRaw: function (number, model) {
+    var args = [].slice.call(arguments, 1);
+    args.unshift(number * this.multiplier);
+    return (NumberFormatter.prototype.fromRaw.apply(this, args) || "0") + this.symbol;
+  },
+
+  /**
+     Takes a string, possibly appended with `symbol` and/or `decimalSeparator`,
+     and convert it back to a number for the model like NumberFormatter#toRaw,
+     and then dividing it by `multiplier`.
+
+     @member Backgrid.PercentFormatter
+     @param {string} formattedData
+     @param {Backbone.Model} model Used for more complicated formatting
+     @return {number|undefined} Undefined if the string cannot be converted to
+     a number.
+  */
+  toRaw: function (formattedValue, model) {
+    var tokens = formattedValue.split(this.symbol);
+    if (tokens && tokens[0] && tokens[1] === "" || tokens[1] == null) {
+      var rawValue = NumberFormatter.prototype.toRaw.call(this, tokens[0]);
+      if (_.isUndefined(rawValue)) return rawValue;
+      return rawValue / this.multiplier;
+    }
+  }
+
+});
+
+/**
+   Formatter to converts between various datetime formats.
+
+   This class only understands ISO-8601 formatted datetime strings and UNIX
+   offset (number of milliseconds since UNIX Epoch). See
+   Backgrid.Extension.MomentFormatter if you need a much more flexible datetime
+   formatter.
+
+   @class Backgrid.DatetimeFormatter
+   @extends Backgrid.CellFormatter
+   @constructor
+   @throws {Error} If both `includeDate` and `includeTime` are false.
+*/
+var DatetimeFormatter = Backgrid.DatetimeFormatter = function (options) {
+  _.extend(this, this.defaults, options || {});
+
+  if (!this.includeDate && !this.includeTime) {
+    throw new Error("Either includeDate or includeTime must be true");
+  }
+};
+DatetimeFormatter.prototype = new CellFormatter();
+_.extend(DatetimeFormatter.prototype, {
+
+  /**
+     @member Backgrid.DatetimeFormatter
+
+     @cfg {Object} options
+
+     @cfg {boolean} [options.includeDate=true] Whether the values include the
+     date part.
+
+     @cfg {boolean} [options.includeTime=true] Whether the values include the
+     time part.
+
+     @cfg {boolean} [options.includeMilli=false] If `includeTime` is true,
+     whether to include the millisecond part, if it exists.
+   */
+  defaults: {
+    includeDate: true,
+    includeTime: true,
+    includeMilli: false
+  },
+
+  DATE_RE: /^([+\-]?\d{4})-(\d{2})-(\d{2})$/,
+  TIME_RE: /^(\d{2}):(\d{2}):(\d{2})(\.(\d{3}))?$/,
+  ISO_SPLITTER_RE: /T|Z| +/,
+
+  _convert: function (data, validate) {
+    if ((data + '').trim() === '') return null;
+
+    var date, time = null;
+    if (_.isNumber(data)) {
+      var jsDate = new Date(data);
+      date = lpad(jsDate.getUTCFullYear(), 4, 0) + '-' + lpad(jsDate.getUTCMonth() + 1, 2, 0) + '-' + lpad(jsDate.getUTCDate(), 2, 0);
+      time = lpad(jsDate.getUTCHours(), 2, 0) + ':' + lpad(jsDate.getUTCMinutes(), 2, 0) + ':' + lpad(jsDate.getUTCSeconds(), 2, 0);
+    }
+    else {
+      data = data.trim();
+      var parts = data.split(this.ISO_SPLITTER_RE) || [];
+      date = this.DATE_RE.test(parts[0]) ? parts[0] : '';
+      time = date && parts[1] ? parts[1] : this.TIME_RE.test(parts[0]) ? parts[0] : '';
+    }
+
+    var YYYYMMDD = this.DATE_RE.exec(date) || [];
+    var HHmmssSSS = this.TIME_RE.exec(time) || [];
+
+    if (validate) {
+      if (this.includeDate && _.isUndefined(YYYYMMDD[0])) return;
+      if (this.includeTime && _.isUndefined(HHmmssSSS[0])) return;
+      if (!this.includeDate && date) return;
+      if (!this.includeTime && time) return;
+    }
+
+    var jsDate = new Date(Date.UTC(YYYYMMDD[1] * 1 || 0,
+                                   YYYYMMDD[2] * 1 - 1 || 0,
+                                   YYYYMMDD[3] * 1 || 0,
+                                   HHmmssSSS[1] * 1 || null,
+                                   HHmmssSSS[2] * 1 || null,
+                                   HHmmssSSS[3] * 1 || null,
+                                   HHmmssSSS[5] * 1 || null));
+
+    var result = '';
+
+    if (this.includeDate) {
+      result = lpad(jsDate.getUTCFullYear(), 4, 0) + '-' + lpad(jsDate.getUTCMonth() + 1, 2, 0) + '-' + lpad(jsDate.getUTCDate(), 2, 0);
+    }
+
+    if (this.includeTime) {
+      result = result + (this.includeDate ? 'T' : '') + lpad(jsDate.getUTCHours(), 2, 0) + ':' + lpad(jsDate.getUTCMinutes(), 2, 0) + ':' + lpad(jsDate.getUTCSeconds(), 2, 0);
+
+      if (this.includeMilli) {
+        result = result + '.' + lpad(jsDate.getUTCMilliseconds(), 3, 0);
+      }
+    }
+
+    if (this.includeDate && this.includeTime) {
+      result += "Z";
+    }
+
+    return result;
+  },
+
+  /**
+     Converts an ISO-8601 formatted datetime string to a datetime string, date
+     string or a time string. The timezone is ignored if supplied.
+
+     @member Backgrid.DatetimeFormatter
+     @param {string} rawData
+     @param {Backbone.Model} model Used for more complicated formatting
+     @return {string|null|undefined} ISO-8601 string in UTC. Null and undefined
+     values are returned as is.
+  */
+  fromRaw: function (rawData, model) {
+    if (_.isNull(rawData) || _.isUndefined(rawData)) return '';
+    return this._convert(rawData);
+  },
+
+  /**
+     Converts an ISO-8601 formatted datetime string to a datetime string, date
+     string or a time string. The timezone is ignored if supplied. This method
+     parses the input values exactly the same way as
+     Backgrid.Extension.MomentFormatter#fromRaw(), in addition to doing some
+     sanity checks.
+
+     @member Backgrid.DatetimeFormatter
+     @param {string} formattedData
+     @param {Backbone.Model} model Used for more complicated formatting
+     @return {string|undefined} ISO-8601 string in UTC. Undefined if a date is
+     found when `includeDate` is false, or a time is found when `includeTime` is
+     false, or if `includeDate` is true and a date is not found, or if
+     `includeTime` is true and a time is not found.
+  */
+  toRaw: function (formattedData, model) {
+    return this._convert(formattedData, true);
+  }
+
+});
+
+/**
+   Formatter to convert any value to string.
+
+   @class Backgrid.StringFormatter
+   @extends Backgrid.CellFormatter
+   @constructor
+ */
+var StringFormatter = Backgrid.StringFormatter = function () {};
+StringFormatter.prototype = new CellFormatter();
+_.extend(StringFormatter.prototype, {
+  /**
+     Converts any value to a string using Ecmascript's implicit type
+     conversion. If the given value is `null` or `undefined`, an empty string is
+     returned instead.
+
+     @member Backgrid.StringFormatter
+     @param {*} rawValue
+     @param {Backbone.Model} model Used for more complicated formatting
+     @return {string}
+   */
+  fromRaw: function (rawValue, model) {
+    if (_.isUndefined(rawValue) || _.isNull(rawValue)) return '';
+    return rawValue + '';
+  }
+});
+
+/**
+   Simple email validation formatter.
+
+   @class Backgrid.EmailFormatter
+   @extends Backgrid.CellFormatter
+   @constructor
+ */
+var EmailFormatter = Backgrid.EmailFormatter = function () {};
+EmailFormatter.prototype = new CellFormatter();
+_.extend(EmailFormatter.prototype, {
+  /**
+     Return the input if it is a string that contains an '@' character and if
+     the strings before and after '@' are non-empty. If the input does not
+     validate, `undefined` is returned.
+
+     @member Backgrid.EmailFormatter
+     @param {*} formattedData
+     @param {Backbone.Model} model Used for more complicated formatting
+     @return {string|undefined}
+   */
+  toRaw: function (formattedData, model) {
+    var parts = formattedData.trim().split("@");
+    if (parts.length === 2 && _.all(parts)) {
+      return formattedData;
+    }
+  }
+});
+
+/**
+   Formatter for SelectCell.
+
+   If the type of a model value is not a string, it is expected that a subclass
+   of this formatter is provided to the SelectCell, with #toRaw overridden to
+   convert the string value returned from the DOM back to whatever value is
+   expected in the model.
+
+   @class Backgrid.SelectFormatter
+   @extends Backgrid.CellFormatter
+   @constructor
+*/
+var SelectFormatter = Backgrid.SelectFormatter = function () {};
+SelectFormatter.prototype = new CellFormatter();
+_.extend(SelectFormatter.prototype, {
+
+  /**
+     Normalizes raw scalar or array values to an array.
+
+     @member Backgrid.SelectFormatter
+     @param {*} rawValue
+     @param {Backbone.Model} model Used for more complicated formatting
+     @return {Array.<*>}
+  */
+  fromRaw: function (rawValue, model) {
+    return _.isArray(rawValue) ? rawValue : rawValue != null ? [rawValue] : [];
+  }
+});
+
+/*
+  backgrid
+  http://github.com/wyuenho/backgrid
+
+  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+  Licensed under the MIT license.
+*/
+
+/**
+   Generic cell editor base class. Only defines an initializer for a number of
+   required parameters.
+
+   @abstract
+   @class Backgrid.CellEditor
+   @extends Backbone.View
+*/
+var CellEditor = Backgrid.CellEditor = Backbone.View.extend({
+
+  /**
+     Initializer.
+
+     @param {Object} options
+     @param {Backgrid.CellFormatter} options.formatter
+     @param {Backgrid.Column} options.column
+     @param {Backbone.Model} options.model
+
+     @throws {TypeError} If `formatter` is not a formatter instance, or when
+     `model` or `column` are undefined.
+  */
+  initialize: function (options) {
+    this.formatter = options.formatter;
+    this.column = options.column;
+    if (!(this.column instanceof Column)) {
+      this.column = new Column(this.column);
+    }
+
+    this.listenTo(this.model, "backgrid:editing", this.postRender);
+  },
+
+  /**
+     Post-rendering setup and initialization. Focuses the cell editor's `el` in
+     this default implementation. **Should** be called by Cell classes after
+     calling Backgrid.CellEditor#render.
+  */
+  postRender: function (model, column) {
+    if (column == null || column.get("name") == this.column.get("name")) {
+      this.$el.focus();
+    }
+    return this;
+  }
+
+});
+
+/**
+   InputCellEditor the cell editor type used by most core cell types. This cell
+   editor renders a text input box as its editor. The input will render a
+   placeholder if the value is empty on supported browsers.
+
+   @class Backgrid.InputCellEditor
+   @extends Backgrid.CellEditor
+*/
+var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({
+
+  /** @property */
+  tagName: "input",
+
+  /** @property */
+  attributes: {
+    type: "text"
+  },
+
+  /** @property */
+  events: {
+    "blur": "saveOrCancel",
+    "keydown": "saveOrCancel"
+  },
+
+  /**
+     Initializer. Removes this `el` from the DOM when a `done` event is
+     triggered.
+
+     @param {Object} options
+     @param {Backgrid.CellFormatter} options.formatter
+     @param {Backgrid.Column} options.column
+     @param {Backbone.Model} options.model
+     @param {string} [options.placeholder]
+  */
+  initialize: function (options) {
+    InputCellEditor.__super__.initialize.apply(this, arguments);
+
+    if (options.placeholder) {
+      this.$el.attr("placeholder", options.placeholder);
+    }
+  },
+
+  /**
+     Renders a text input with the cell value formatted for display, if it
+     exists.
+  */
+  render: function () {
+    var model = this.model;
+    this.$el.val(this.formatter.fromRaw(model.get(this.column.get("name")), model));
+    return this;
+  },
+
+  /**
+     If the key pressed is `enter`, `tab`, `up`, or `down`, converts the value
+     in the editor to a raw value for saving into the model using the formatter.
+
+     If the key pressed is `esc` the changes are undone.
+
+     If the editor goes out of focus (`blur`) but the value is invalid, the
+     event is intercepted and cancelled so the cell remains in focus pending for
+     further action. The changes are saved otherwise.
+
+     Triggers a Backbone `backgrid:edited` event from the model when successful,
+     and `backgrid:error` if the value cannot be converted. Classes listening to
+     the `error` event, usually the Cell classes, should respond appropriately,
+     usually by rendering some kind of error feedback.
+
+     @param {Event} e
+  */
+  saveOrCancel: function (e) {
+
+    var formatter = this.formatter;
+    var model = this.model;
+    var column = this.column;
+
+    var command = new Command(e);
+    var blurred = e.type === "blur";
+
+    if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() ||
+        command.save() || blurred) {
+
+      e.preventDefault();
+      e.stopPropagation();
+
+      var val = this.$el.val();
+      var newValue = formatter.toRaw(val, model);
+      if (_.isUndefined(newValue)) {
+        model.trigger("backgrid:error", model, column, val);
+      }
+      else {
+        model.set(column.get("name"), newValue);
+        model.trigger("backgrid:edited", model, column, command);
+      }
+    }
+    // esc
+    else if (command.cancel()) {
+      // undo
+      e.stopPropagation();
+      model.trigger("backgrid:edited", model, column, command);
+    }
+  },
+
+  postRender: function (model, column) {
+    if (column == null || column.get("name") == this.column.get("name")) {
+      // move the cursor to the end on firefox if text is right aligned
+      if (this.$el.css("text-align") === "right") {
+        var val = this.$el.val();
+        this.$el.focus().val(null).val(val);
+      }
+      else this.$el.focus();
+    }
+    return this;
+  }
+
+});
+
+/**
+   The super-class for all Cell types. By default, this class renders a plain
+   table cell with the model value converted to a string using the
+   formatter. The table cell is clickable, upon which the cell will go into
+   editor mode, which is rendered by a Backgrid.InputCellEditor instance by
+   default. Upon encountering any formatting errors, this class will add an
+   `error` CSS class to the table cell.
+
+   @abstract
+   @class Backgrid.Cell
+   @extends Backbone.View
+*/
+var Cell = Backgrid.Cell = Backbone.View.extend({
+
+  /** @property */
+  tagName: "td",
+
+  /**
+     @property {Backgrid.CellFormatter|Object|string} [formatter=CellFormatter]
+  */
+  formatter: CellFormatter,
+
+  /**
+     @property {Backgrid.CellEditor} [editor=Backgrid.InputCellEditor] The
+     default editor for all cell instances of this class. This value must be a
+     class, it will be automatically instantiated upon entering edit mode.
+
+     See Backgrid.CellEditor
+  */
+  editor: InputCellEditor,
+
+  /** @property */
+  events: {
+    "click": "enterEditMode"
+  },
+
+  /**
+     Initializer.
+
+     @param {Object} options
+     @param {Backbone.Model} options.model
+     @param {Backgrid.Column} options.column
+
+     @throws {ReferenceError} If formatter is a string but a formatter class of
+     said name cannot be found in the Backgrid module.
+  */
+  initialize: function (options) {
+    this.column = options.column;
+    if (!(this.column instanceof Column)) {
+      this.column = new Column(this.column);
+    }
+
+    var column = this.column, model = this.model, $el = this.$el;
+
+    var formatter = Backgrid.resolveNameToClass(column.get("formatter") ||
+                                                this.formatter, "Formatter");
+
+    if (!_.isFunction(formatter.fromRaw) && !_.isFunction(formatter.toRaw)) {
+      formatter = new formatter();
+    }
+
+    this.formatter = formatter;
+
+    this.editor = Backgrid.resolveNameToClass(this.editor, "CellEditor");
+
+    this.listenTo(model, "change:" + column.get("name"), function () {
+      if (!$el.hasClass("editor")) this.render();
+    });
+
+    this.listenTo(model, "backgrid:error", this.renderError);
+
+    this.listenTo(column, "change:editable change:sortable change:renderable",
+                  function (column) {
+                    var changed = column.changedAttributes();
+                    for (var key in changed) {
+                      if (changed.hasOwnProperty(key)) {
+                        $el.toggleClass(key, changed[key]);
+                      }
+                    }
+                  });
+
+    if (Backgrid.callByNeed(column.editable(), column, model)) $el.addClass("editable");
+    if (Backgrid.callByNeed(column.sortable(), column, model)) $el.addClass("sortable");
+    if (Backgrid.callByNeed(column.renderable(), column, model)) $el.addClass("renderable");
+  },
+
+  /**
+     Render a text string in a table cell. The text is converted from the
+     model's raw value for this cell's column.
+  */
+  render: function () {
+    this.$el.empty();
+    var model = this.model;
+    this.$el.text(this.formatter.fromRaw(model.get(this.column.get("name")), model));
+    this.delegateEvents();
+    return this;
+  },
+
+  /**
+     If this column is editable, a new CellEditor instance is instantiated with
+     its required parameters. An `editor` CSS class is added to the cell upon
+     entering edit mode.
+
+     This method triggers a Backbone `backgrid:edit` event from the model when
+     the cell is entering edit mode and an editor instance has been constructed,
+     but before it is rendered and inserted into the DOM. The cell and the
+     constructed cell editor instance are sent as event parameters when this
+     event is triggered.
+
+     When this cell has finished switching to edit mode, a Backbone
+     `backgrid:editing` event is triggered from the model. The cell and the
+     constructed cell instance are also sent as parameters in the event.
+
+     When the model triggers a `backgrid:error` event, it means the editor is
+     unable to convert the current user input to an apprpriate value for the
+     model's column, and an `error` CSS class is added to the cell accordingly.
+  */
+  enterEditMode: function () {
+    var model = this.model;
+    var column = this.column;
+
+    var editable = Backgrid.callByNeed(column.editable(), column, model);
+    if (editable) {
+
+      this.currentEditor = new this.editor({
+        column: this.column,
+        model: this.model,
+        formatter: this.formatter
+      });
+
+      model.trigger("backgrid:edit", model, column, this, this.currentEditor);
+
+      // Need to redundantly undelegate events for Firefox
+      this.undelegateEvents();
+      this.$el.empty();
+      this.$el.append(this.currentEditor.$el);
+      this.currentEditor.render();
+      this.$el.addClass("editor");
+
+      model.trigger("backgrid:editing", model, column, this, this.currentEditor);
+    }
+  },
+
+  /**
+     Put an `error` CSS class on the table cell.
+  */
+  renderError: function (model, column) {
+    if (column == null || column.get("name") == this.column.get("name")) {
+      this.$el.addClass("error");
+    }
+  },
+
+  /**
+     Removes the editor and re-render in display mode.
+  */
+  exitEditMode: function () {
+    this.$el.removeClass("error");
+    this.currentEditor.remove();
+    this.stopListening(this.currentEditor);
+    delete this.currentEditor;
+    this.$el.removeClass("editor");
+    this.render();
+  },
+
+  /**
+     Clean up this cell.
+
+     @chainable
+  */
+  remove: function () {
+    if (this.currentEditor) {
+      this.currentEditor.remove.apply(this.currentEditor, arguments);
+      delete this.currentEditor;
+    }
+    return Cell.__super__.remove.apply(this, arguments);
+  }
+
+});
+
+/**
+   StringCell displays HTML escaped strings and accepts anything typed in.
+
+   @class Backgrid.StringCell
+   @extends Backgrid.Cell
+*/
+var StringCell = Backgrid.StringCell = Cell.extend({
+
+  /** @property */
+  className: "string-cell",
+
+  formatter: StringFormatter
+
+});
+
+/**
+   UriCell renders an HTML `<a>` anchor for the value and accepts URIs as user
+   input values. No type conversion or URL validation is done by the formatter
+   of this cell. Users who need URL validation are encourage to subclass UriCell
+   to take advantage of the parsing capabilities of the HTMLAnchorElement
+   available on HTML5-capable browsers or using a third-party library like
+   [URI.js](https://github.com/medialize/URI.js).
+
+   @class Backgrid.UriCell
+   @extends Backgrid.Cell
+*/
+var UriCell = Backgrid.UriCell = Cell.extend({
+
+  /** @property */
+  className: "uri-cell",
+
+  /**
+     @property {string} [title] The title attribute of the generated anchor. It
+     uses the display value formatted by the `formatter.fromRaw` by default.
+  */
+  title: null,
+
+  /**
+     @property {string} [target="_blank"] The target attribute of the generated
+     anchor.
+  */
+  target: "_blank",
+
+  initialize: function (options) {
+    UriCell.__super__.initialize.apply(this, arguments);
+    this.title = options.title || this.title;
+    this.target = options.target || this.target;
+  },
+
+  render: function () {
+    this.$el.empty();
+    var rawValue = this.model.get(this.column.get("name"));
+    var formattedValue = this.formatter.fromRaw(rawValue, this.model);
+    this.$el.append($("<a>", {
+      tabIndex: -1,
+      href: rawValue,
+      title: this.title || formattedValue,
+      target: this.target
+    }).text(formattedValue));
+    this.delegateEvents();
+    return this;
+  }
+
+});
+
+/**
+   Like Backgrid.UriCell, EmailCell renders an HTML `<a>` anchor for the
+   value. The `href` in the anchor is prefixed with `mailto:`. EmailCell will
+   complain if the user enters a string that doesn't contain the `@` sign.
+
+   @class Backgrid.EmailCell
+   @extends Backgrid.StringCell
+*/
+var EmailCell = Backgrid.EmailCell = StringCell.extend({
+
+  /** @property */
+  className: "email-cell",
+
+  formatter: EmailFormatter,
+
+  render: function () {
+    this.$el.empty();
+    var model = this.model;
+    var formattedValue = this.formatter.fromRaw(model.get(this.column.get("name")), model);
+    this.$el.append($("<a>", {
+      tabIndex: -1,
+      href: "mailto:" + formattedValue,
+      title: formattedValue
+    }).text(formattedValue));
+    this.delegateEvents();
+    return this;
+  }
+
+});
+
+/**
+   NumberCell is a generic cell that renders all numbers. Numbers are formatted
+   using a Backgrid.NumberFormatter.
+
+   @class Backgrid.NumberCell
+   @extends Backgrid.Cell
+*/
+var NumberCell = Backgrid.NumberCell = Cell.extend({
+
+  /** @property */
+  className: "number-cell",
+
+  /**
+     @property {number} [decimals=2] Must be an integer.
+  */
+  decimals: NumberFormatter.prototype.defaults.decimals,
+
+  /** @property {string} [decimalSeparator='.'] */
+  decimalSeparator: NumberFormatter.prototype.defaults.decimalSeparator,
+
+  /** @property {string} [orderSeparator=','] */
+  orderSeparator: NumberFormatter.prototype.defaults.orderSeparator,
+
+  /** @property {Backgrid.CellFormatter} [formatter=Backgrid.NumberFormatter] */
+  formatter: NumberFormatter,
+
+  /**
+     Initializes this cell and the number formatter.
+
+     @param {Object} options
+     @param {Backbone.Model} options.model
+     @param {Backgrid.Column} options.column
+  */
+  initialize: function (options) {
+    NumberCell.__super__.initialize.apply(this, arguments);
+    var formatter = this.formatter;
+    formatter.decimals = this.decimals;
+    formatter.decimalSeparator = this.decimalSeparator;
+    formatter.orderSeparator = this.orderSeparator;
+  }
+
+});
+
+/**
+   An IntegerCell is just a Backgrid.NumberCell with 0 decimals. If a floating
+   point number is supplied, the number is simply rounded the usual way when
+   displayed.
+
+   @class Backgrid.IntegerCell
+   @extends Backgrid.NumberCell
+*/
+var IntegerCell = Backgrid.IntegerCell = NumberCell.extend({
+
+  /** @property */
+  className: "integer-cell",
+
+  /**
+     @property {number} decimals Must be an integer.
+  */
+  decimals: 0
+});
+
+/**
+   A PercentCell is another Backgrid.NumberCell that takes a floating number,
+   optionally multiplied by a multiplier and display it as a percentage.
+
+   @class Backgrid.PercentCell
+   @extends Backgrid.NumberCell
+ */
+var PercentCell = Backgrid.PercentCell = NumberCell.extend({
+
+  /** @property */
+  className: "percent-cell",
+
+  /** @property {number} [multiplier=1] */
+  multiplier: PercentFormatter.prototype.defaults.multiplier,
+
+  /** @property {string} [symbol='%'] */
+  symbol: PercentFormatter.prototype.defaults.symbol,
+
+  /** @property {Backgrid.CellFormatter} [formatter=Backgrid.PercentFormatter] */
+  formatter: PercentFormatter,
+
+  /**
+     Initializes this cell and the percent formatter.
+
+     @param {Object} options
+     @param {Backbone.Model} options.model
+     @param {Backgrid.Column} options.column
+  */
+  initialize: function () {
+    PercentCell.__super__.initialize.apply(this, arguments);
+    var formatter = this.formatter;
+    formatter.multiplier = this.multiplier;
+    formatter.symbol = this.symbol;
+  }
+
+});
+
+/**
+   DatetimeCell is a basic cell that accepts datetime string values in RFC-2822
+   or W3C's subset of ISO-8601 and displays them in ISO-8601 format. For a much
+   more sophisticated date time cell with better datetime formatting, take a
+   look at the Backgrid.Extension.MomentCell extension.
+
+   @class Backgrid.DatetimeCell
+   @extends Backgrid.Cell
+
+   See:
+
+   - Backgrid.Extension.MomentCell
+   - Backgrid.DatetimeFormatter
+*/
+var DatetimeCell = Backgrid.DatetimeCell = Cell.extend({
+
+  /** @property */
+  className: "datetime-cell",
+
+  /**
+     @property {boolean} [includeDate=true]
+  */
+  includeDate: DatetimeFormatter.prototype.defaults.includeDate,
+
+  /**
+     @property {boolean} [includeTime=true]
+  */
+  includeTime: DatetimeFormatter.prototype.defaults.includeTime,
+
+  /**
+     @property {boolean} [includeMilli=false]
+  */
+  includeMilli: DatetimeFormatter.prototype.defaults.includeMilli,
+
+  /** @property {Backgrid.CellFormatter} [formatter=Backgrid.DatetimeFormatter] */
+  formatter: DatetimeFormatter,
+
+  /**
+     Initializes this cell and the datetime formatter.
+
+     @param {Object} options
+     @param {Backbone.Model} options.model
+     @param {Backgrid.Column} options.column
+  */
+  initialize: function (options) {
+    DatetimeCell.__super__.initialize.apply(this, arguments);
+    var formatter = this.formatter;
+    formatter.includeDate = this.includeDate;
+    formatter.includeTime = this.includeTime;
+    formatter.includeMilli = this.includeMilli;
+
+    var placeholder = this.includeDate ? "YYYY-MM-DD" : "";
+    placeholder += (this.includeDate && this.includeTime) ? "T" : "";
+    placeholder += this.includeTime ? "HH:mm:ss" : "";
+    placeholder += (this.includeTime && this.includeMilli) ? ".SSS" : "";
+
+    this.editor = this.editor.extend({
+      attributes: _.extend({}, this.editor.prototype.attributes, this.editor.attributes, {
+        placeholder: placeholder
+      })
+    });
+  }
+
+});
+
+/**
+   DateCell is a Backgrid.DatetimeCell without the time part.
+
+   @class Backgrid.DateCell
+   @extends Backgrid.DatetimeCell
+*/
+var DateCell = Backgrid.DateCell = DatetimeCell.extend({
+
+  /** @property */
+  className: "date-cell",
+
+  /** @property */
+  includeTime: false
+
+});
+
+/**
+   TimeCell is a Backgrid.DatetimeCell without the date part.
+
+   @class Backgrid.TimeCell
+   @extends Backgrid.DatetimeCell
+*/
+var TimeCell = Backgrid.TimeCell = DatetimeCell.extend({
+
+  /** @property */
+  className: "time-cell",
+
+  /** @property */
+  includeDate: false
+
+});
+
+/**
+   BooleanCellEditor renders a checkbox as its editor.
+
+   @class Backgrid.BooleanCellEditor
+   @extends Backgrid.CellEditor
+*/
+var BooleanCellEditor = Backgrid.BooleanCellEditor = CellEditor.extend({
+
+  /** @property */
+  tagName: "input",
+
+  /** @property */
+  attributes: {
+    tabIndex: -1,
+    type: "checkbox"
+  },
+
+  /** @property */
+  events: {
+    "mousedown": function () {
+      this.mouseDown = true;
+    },
+    "blur": "enterOrExitEditMode",
+    "mouseup": function () {
+      this.mouseDown = false;
+    },
+    "change": "saveOrCancel",
+    "keydown": "saveOrCancel"
+  },
+
+  /**
+     Renders a checkbox and check it if the model value of this column is true,
+     uncheck otherwise.
+  */
+  render: function () {
+    var model = this.model;
+    var val = this.formatter.fromRaw(model.get(this.column.get("name")), model);
+    this.$el.prop("checked", val);
+    return this;
+  },
+
+  /**
+     Event handler. Hack to deal with the case where `blur` is fired before
+     `change` and `click` on a checkbox.
+  */
+  enterOrExitEditMode: function (e) {
+    if (!this.mouseDown) {
+      var model = this.model;
+      model.trigger("backgrid:edited", model, this.column, new Command(e));
+    }
+  },
+
+  /**
+     Event handler. Save the value into the model if the event is `change` or
+     one of the keyboard navigation key presses. Exit edit mode without saving
+     if `escape` was pressed.
+  */
+  saveOrCancel: function (e) {
+    var model = this.model;
+    var column = this.column;
+    var formatter = this.formatter;
+    var command = new Command(e);
+    // skip ahead to `change` when space is pressed
+    if (command.passThru() && e.type != "change") return true;
+    if (command.cancel()) {
+      e.stopPropagation();
+      model.trigger("backgrid:edited", model, column, command);
+    }
+
+    var $el = this.$el;
+    if (command.save() || command.moveLeft() || command.moveRight() || command.moveUp() ||
+        command.moveDown()) {
+      e.preventDefault();
+      e.stopPropagation();
+      var val = formatter.toRaw($el.prop("checked"), model);
+      model.set(column.get("name"), val);
+      model.trigger("backgrid:edited", model, column, command);
+    }
+    else if (e.type == "change") {
+      var val = formatter.toRaw($el.prop("checked"), model);
+      model.set(column.get("name"), val);
+      $el.focus();
+    }
+  }
+
+});
+
+/**
+   BooleanCell renders a checkbox both during display mode and edit mode. The
+   checkbox is checked if the model value is true, unchecked otherwise.
+
+   @class Backgrid.BooleanCell
+   @extends Backgrid.Cell
+*/
+var BooleanCell = Backgrid.BooleanCell = Cell.extend({
+
+  /** @property */
+  className: "boolean-cell",
+
+  /** @property */
+  editor: BooleanCellEditor,
+
+  /** @property */
+  events: {
+    "click": "enterEditMode"
+  },
+
+  /**
+     Renders a checkbox and check it if the model value of this column is true,
+     uncheck otherwise.
+  */
+  render: function () {
+    this.$el.empty();
+    var model = this.model, column = this.column;
+    var editable = Backgrid.callByNeed(column.editable(), column, model);
+    this.$el.append($("<input>", {
+      tabIndex: -1,
+      type: "checkbox",
+      checked: this.formatter.fromRaw(model.get(column.get("name")), model),
+      disabled: !editable
+    }));
+    this.delegateEvents();
+    return this;
+  }
+
+});
+
+/**
+   SelectCellEditor renders an HTML `<select>` fragment as the editor.
+
+   @class Backgrid.SelectCellEditor
+   @extends Backgrid.CellEditor
+*/
+var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({
+
+  /** @property */
+  tagName: "select",
+
+  /** @property */
+  events: {
+    "change": "save",
+    "blur": "close",
+    "keydown": "close"
+  },
+
+  /** @property {function(Object, ?Object=): string} template */
+  template: _.template('<option value="<%- value %>" <%= selected ? \'selected="selected"\' : "" %>><%- text %></option>', null, {variable: null}),
+
+  setOptionValues: function (optionValues) {
+    this.optionValues = optionValues;
+    this.optionValues = _.result(this, "optionValues");
+  },
+
+  setMultiple: function (multiple) {
+    this.multiple = multiple;
+    this.$el.prop("multiple", multiple);
+  },
+
+  _renderOptions: function (nvps, selectedValues) {
+    var options = '';
+    for (var i = 0; i < nvps.length; i++) {
+      options = options + this.template({
+        text: nvps[i][0],
+        value: nvps[i][1],
+        selected: _.indexOf(selectedValues, nvps[i][1]) > -1
+      });
+    }
+    return options;
+  },
+
+  /**
+     Renders the options if `optionValues` is a list of name-value pairs. The
+     options are contained inside option groups if `optionValues` is a list of
+     object hashes. The name is rendered at the option text and the value is the
+     option value. If `optionValues` is a function, it is called without a
+     parameter.
+  */
+  render: function () {
+    this.$el.empty();
+
+    var optionValues = _.result(this, "optionValues");
+    var model = this.model;
+    var selectedValues = this.formatter.fromRaw(model.get(this.column.get("name")), model);
+
+    if (!_.isArray(optionValues)) throw new TypeError("optionValues must be an array");
+
+    var optionValue = null;
+    var optionText = null;
+    var optionValue = null;
+    var optgroupName = null;
+    var optgroup = null;
+
+    for (var i = 0; i < optionValues.length; i++) {
+      var optionValue = optionValues[i];
+
+      if (_.isArray(optionValue)) {
+        optionText  = optionValue[0];
+        optionValue = optionValue[1];
+
+        this.$el.append(this.template({
+          text: optionText,
+          value: optionValue,
+          selected: _.indexOf(selectedValues, optionValue) > -1
+        }));
+      }
+      else if (_.isObject(optionValue)) {
+        optgroupName = optionValue.name;
+        optgroup = $("<optgroup></optgroup>", { label: optgroupName });
+        optgroup.append(this._renderOptions.call(this, optionValue.values, selectedValues));
+        this.$el.append(optgroup);
+      }
+      else {
+        throw new TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }");
+      }
+    }
+
+    this.delegateEvents();
+
+    return this;
+  },
+
+  /**
+     Saves the value of the selected option to the model attribute.
+  */
+  save: function (e) {
+    var model = this.model;
+    var column = this.column;
+    model.set(column.get("name"), this.formatter.toRaw(this.$el.val(), model));
+  },
+
+  /**
+     Triggers a `backgrid:edited` event from the model so the body can close
+     this editor.
+  */
+  close: function (e) {
+    var model = this.model;
+    var column = this.column;
+    var command = new Command(e);
+    if (command.cancel()) {
+      e.stopPropagation();
+      model.trigger("backgrid:edited", model, column, new Command(e));
+    }
+    else if (command.save() || command.moveLeft() || command.moveRight() ||
+             command.moveUp() || command.moveDown() || e.type == "blur") {
+      e.preventDefault();
+      e.stopPropagation();
+      this.save(e);
+      model.trigger("backgrid:edited", model, column, new Command(e));
+    }
+  }
+
+});
+
+/**
+   SelectCell is also a different kind of cell in that upon going into edit mode
+   the cell renders a list of options to pick from, as opposed to an input box.
+
+   SelectCell cannot be referenced by its string name when used in a column
+   definition because it requires an `optionValues` class attribute to be
+   defined. `optionValues` can either be a list of name-value pairs, to be
+   rendered as options, or a list of object hashes which consist of a key *name*
+   which is the option group name, and a key *values* which is a list of
+   name-value pairs to be rendered as options under that option group.
+
+   In addition, `optionValues` can also be a parameter-less function that
+   returns one of the above. If the options are static, it is recommended the
+   returned values to be memoized. `_.memoize()` is a good function to help with
+   that.
+
+   During display mode, the default formatter will normalize the raw model value
+   to an array of values whether the raw model value is a scalar or an
+   array. Each value is compared with the `optionValues` values using
+   Ecmascript's implicit type conversion rules. When exiting edit mode, no type
+   conversion is performed when saving into the model. This behavior is not
+   always desirable when the value type is anything other than string. To
+   control type conversion on the client-side, you should subclass SelectCell to
+   provide a custom formatter or provide the formatter to your column
+   definition.
+
+   See:
+     [$.fn.val()](http://api.jquery.com/val/)
+
+   @class Backgrid.SelectCell
+   @extends Backgrid.Cell
+*/
+var SelectCell = Backgrid.SelectCell = Cell.extend({
+
+  /** @property */
+  className: "select-cell",
+
+  /** @property */
+  editor: SelectCellEditor,
+
+  /** @property */
+  multiple: false,
+
+  /** @property */
+  formatter: SelectFormatter,
+
+  /**
+     @property {Array.<Array>|Array.<{name: string, values: Array.<Array>}>} optionValues
+  */
+  optionValues: undefined,
+
+  /** @property */
+  delimiter: ', ',
+
+  /**
+     Initializer.
+
+     @param {Object} options
+     @param {Backbone.Model} options.model
+     @param {Backgrid.Column} options.column
+
+     @throws {TypeError} If `optionsValues` is undefined.
+  */
+  initialize: function (options) {
+    SelectCell.__super__.initialize.apply(this, arguments);
+    this.listenTo(this.model, "backgrid:edit", function (model, column, cell, editor) {
+      if (column.get("name") == this.column.get("name")) {
+        editor.setOptionValues(this.optionValues);
+        editor.setMultiple(this.multiple);
+      }
+    });
+  },
+
+  /**
+     Renders the label using the raw value as key to look up from `optionValues`.
+
+     @throws {TypeError} If `optionValues` is malformed.
+  */
+  render: function () {
+    this.$el.empty();
+
+    var optionValues = _.result(this, "optionValues");
+    var model = this.model;
+    var rawData = this.formatter.fromRaw(model.get(this.column.get("name")), model);
+
+    var selectedText = [];
+
+    try {
+      if (!_.isArray(optionValues) || _.isEmpty(optionValues)) throw new TypeError;
+
+      for (var k = 0; k < rawData.length; k++) {
+        var rawDatum = rawData[k];
+
+        for (var i = 0; i < optionValues.length; i++) {
+          var optionValue = optionValues[i];
+
+          if (_.isArray(optionValue)) {
+            var optionText  = optionValue[0];
+            var optionValue = optionValue[1];
+
+            if (optionValue == rawDatum) selectedText.push(optionText);
+          }
+          else if (_.isObject(optionValue)) {
+            var optionGroupValues = optionValue.values;
+
+            for (var j = 0; j < optionGroupValues.length; j++) {
+              var optionGroupValue = optionGroupValues[j];
+              if (optionGroupValue[1] == rawDatum) {
+                selectedText.push(optionGroupValue[0]);
+              }
+            }
+          }
+          else {
+            throw new TypeError;
+          }
+        }
+      }
+
+      this.$el.append(selectedText.join(this.delimiter));
+    }
+    catch (ex) {
+      if (ex instanceof TypeError) {
+        throw new TypeError("'optionValues' must be of type {Array.<Array>|Array.<{name: string, values: Array.<Array>}>}");
+      }
+      throw ex;
+    }
+
+    this.delegateEvents();
+
+    return this;
+  }
+
+});
+
+/*
+  backgrid
+  http://github.com/wyuenho/backgrid
+
+  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+  Licensed under the MIT license.
+*/
+
+/**
+   A Column is a placeholder for column metadata.
+
+   You usually don't need to create an instance of this class yourself as a
+   collection of column instances will be created for you from a list of column
+   attributes in the Backgrid.js view class constructors.
+
+   @class Backgrid.Column
+   @extends Backbone.Model
+*/
+var Column = Backgrid.Column = Backbone.Model.extend({
+
+  /**
+     @cfg {Object} defaults Column defaults. To override any of these default
+     values, you can either change the prototype directly to override
+     Column.defaults globally or extend Column and supply the custom class to
+     Backgrid.Grid:
+
+         // Override Column defaults globally
+         Column.prototype.defaults.sortable = false;
+
+         // Override Column defaults locally
+         var MyColumn = Column.extend({
+           defaults: _.defaults({
+             editable: false
+           }, Column.prototype.defaults)
+         });
+
+         var grid = new Backgrid.Grid(columns: new Columns([{...}, {...}], {
+           model: MyColumn
+         }));
+
+     @cfg {string} [defaults.name] The default name of the model attribute.
+
+     @cfg {string} [defaults.label] The default label to show in the header.
+
+     @cfg {string|Backgrid.Cell} [defaults.cell] The default cell type. If this
+     is a string, the capitalized form will be used to look up a cell class in
+     Backbone, i.e.: string => StringCell. If a Cell subclass is supplied, it is
+     initialized with a hash of parameters. If a Cell instance is supplied, it
+     is used directly.
+
+     @cfg {string|Backgrid.HeaderCell} [defaults.headerCell] The default header
+     cell type.
+
+     @cfg {boolean|string|function(): boolean} [defaults.sortable=true] Whether
+     this column is sortable. If the value is a string, a method will the same
+     name will be looked up from the column instance to determine whether the
+     column should be sortable. The method's signature must be `function
+     (Backgrid.Column, Backbone.Model): boolean`.
+
+     @cfg {boolean|string|function(): boolean} [defaults.editable=true] Whether
+     this column is editable. If the value is a string, a method will the same
+     name will be looked up from the column instance to determine whether the
+     column should be editable. The method's signature must be `function
+     (Backgrid.Column, Backbone.Model): boolean`.
+
+     @cfg {boolean|string|function(): boolean} [defaults.renderable=true]
+     Whether this column is renderable. If the value is a string, a method will
+     the same name will be looked up from the column instance to determine
+     whether the column should be renderable. The method's signature must be
+     `function (Backrid.Column, Backbone.Model): boolean`.
+
+     @cfg {Backgrid.CellFormatter | Object | string} [defaults.formatter] The
+     formatter to use to convert between raw model values and user input.
+
+     @cfg {"toggle"|"cycle"} [defaults.sortType="cycle"] Whether sorting will
+     toggle between ascending and descending order, or cycle between insertion
+     order, ascending and descending order.
+
+     @cfg {(function(Backbone.Model, string): *) | string} [defaults.sortValue]
+     The function to use to extract a value from the model for comparison during
+     sorting. If this value is a string, a method with the same name will be
+     looked up from the column instance.
+
+     @cfg {"ascending"|"descending"|null} [defaults.direction=null] The initial
+     sorting direction for this column. The default is ordered by
+     Backbone.Model.cid, which usually means the collection is ordered by
+     insertion order.
+  */
+  defaults: {
+    name: undefined,
+    label: undefined,
+    sortable: true,
+    editable: true,
+    renderable: true,
+    formatter: undefined,
+    sortType: "cycle",
+    sortValue: undefined,
+    direction: null,
+    cell: undefined,
+    headerCell: undefined
+  },
+
+  /**
+     Initializes this Column instance.
+
+     @param {Object} attrs
+
+     @param {string} attrs.name The model attribute this column is responsible
+     for.
+
+     @param {string|Backgrid.Cell} attrs.cell The cell type to use to render
+     this column.
+
+     @param {string} [attrs.label]
+
+     @param {string|Backgrid.HeaderCell} [attrs.headerCell]
+
+     @param {boolean|string|function(): boolean} [attrs.sortable=true]
+
+     @param {boolean|string|function(): boolean} [attrs.editable=true]
+
+     @param {boolean|string|function(): boolean} [attrs.renderable=true]
+
+     @param {Backgrid.CellFormatter | Object | string} [attrs.formatter]
+
+     @param {"toggle"|"cycle"}  [attrs.sortType="cycle"]
+
+     @param {(function(Backbone.Model, string): *) | string} [attrs.sortValue]
+
+     @throws {TypeError} If attrs.cell or attrs.options are not supplied.
+
+     @throws {ReferenceError} If formatter is a string but a formatter class of
+     said name cannot be found in the Backgrid module.
+
+     See:
+
+     - Backgrid.Column.defaults
+     - Backgrid.Cell
+     - Backgrid.CellFormatter
+   */
+  initialize: function () {
+    if (!this.has("label")) {
+      this.set({ label: this.get("name") }, { silent: true });
+    }
+
+    var headerCell = Backgrid.resolveNameToClass(this.get("headerCell"), "HeaderCell");
+
+    var cell = Backgrid.resolveNameToClass(this.get("cell"), "Cell");
+
+    this.set({cell: cell, headerCell: headerCell}, { silent: true });
+  },
+
+  /**
+     Returns an appropriate value extraction function from a model for sorting.
+
+     If the column model contains an attribute `sortValue`, if it is a string, a
+     method from the column instance identifified by the `sortValue` string is
+     returned. If it is a function, it it returned as is. If `sortValue` isn't
+     found from the column model's attributes, a default value extraction
+     function is returned which will compare according to the natural order of
+     the value's type.
+
+     @return {function(Backbone.Model, string): *}
+   */
+  sortValue: function () {
+    var sortValue = this.get("sortValue");
+    if (_.isString(sortValue)) return this[sortValue];
+    else if (_.isFunction(sortValue)) return sortValue;
+
+    return function (model, colName) {
+      return model.get(colName);
+    };
+  }
+
+  /**
+     @member Backgrid.Column
+     @protected
+     @method sortable
+     @return {function(Backgrid.Column, Backbone.Model): boolean | boolean}
+  */
+
+  /**
+     @member Backgrid.Column
+     @protected
+     @method editable
+     @return {function(Backgrid.Column, Backbone.Model): boolean | boolean}
+  */
+
+  /**
+     @member Backgrid.Column
+     @protected
+     @method renderable
+     @return {function(Backgrid.Column, Backbone.Model): boolean | boolean}
+  */
+});
+
+_.each(["sortable", "renderable", "editable"], function (key) {
+  Column.prototype[key] = function () {
+    var value = this.get(key);
+    if (_.isString(value)) return this[value];
+    else if (_.isFunction(value)) return value;
+
+    return !!value;
+  };
+});
+
+/**
+   A Backbone collection of Column instances.
+
+   @class Backgrid.Columns
+   @extends Backbone.Collection
+ */
+var Columns = Backgrid.Columns = Backbone.Collection.extend({
+
+  /**
+     @property {Backgrid.Column} model
+   */
+  model: Column
+});
+
+/*
+  backgrid
+  http://github.com/wyuenho/backgrid
+
+  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+  Licensed under the MIT license.
+*/
+
+/**
+   Row is a simple container view that takes a model instance and a list of
+   column metadata describing how each of the model's attribute is to be
+   rendered, and apply the appropriate cell to each attribute.
+
+   @class Backgrid.Row
+   @extends Backbone.View
+*/
+var Row = Backgrid.Row = Backbone.View.extend({
+
+  /** @property */
+  tagName: "tr",
+
+  /**
+     Initializes a row view instance.
+
+     @param {Object} options
+     @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
+     @param {Backbone.Model} options.model The model instance to render.
+
+     @throws {TypeError} If options.columns or options.model is undefined.
+  */
+  initialize: function (options) {
+
+    var columns = this.columns = options.columns;
+    if (!(columns instanceof Backbone.Collection)) {
+      columns = this.columns = new Columns(columns);
+    }
+
+    var cells = this.cells = [];
+    for (var i = 0; i < columns.length; i++) {
+      cells.push(this.makeCell(columns.at(i), options));
+    }
+
+    this.listenTo(columns, "add", function (column, columns) {
+      var i = columns.indexOf(column);
+      var cell = this.makeCell(column, options);
+      cells.splice(i, 0, cell);
+
+      var $el = this.$el;
+      if (i === 0) {
+        $el.prepend(cell.render().$el);
+      }
+      else if (i === columns.length - 1) {
+        $el.append(cell.render().$el);
+      }
+      else {
+        $el.children().eq(i).before(cell.render().$el);
+      }
+    });
+
+    this.listenTo(columns, "remove", function (column, columns, opts) {
+      cells[opts.index].remove();
+      cells.splice(opts.index, 1);
+    });
+  },
+
+  /**
+     Factory method for making a cell. Used by #initialize internally. Override
+     this to provide an appropriate cell instance for a custom Row subclass.
+
+     @protected
+
+     @param {Backgrid.Column} column
+     @param {Object} options The options passed to #initialize.
+
+     @return {Backgrid.Cell}
+  */
+  makeCell: function (column) {
+    return new (column.get("cell"))({
+      column: column,
+      model: this.model
+    });
+  },
+
+  /**
+     Renders a row of cells for this row's model.
+  */
+  render: function () {
+    this.$el.empty();
+
+    var fragment = document.createDocumentFragment();
+    for (var i = 0; i < this.cells.length; i++) {
+      fragment.appendChild(this.cells[i].render().el);
+    }
+
+    this.el.appendChild(fragment);
+
+    this.delegateEvents();
+
+    return this;
+  },
+
+  /**
+     Clean up this row and its cells.
+
+     @chainable
+  */
+  remove: function () {
+    for (var i = 0; i < this.cells.length; i++) {
+      var cell = this.cells[i];
+      cell.remove.apply(cell, arguments);
+    }
+    return Backbone.View.prototype.remove.apply(this, arguments);
+  }
+
+});
+
+/**
+   EmptyRow is a simple container view that takes a list of column and render a
+   row with a single column.
+
+   @class Backgrid.EmptyRow
+   @extends Backbone.View
+*/
+var EmptyRow = Backgrid.EmptyRow = Backbone.View.extend({
+
+  /** @property */
+  tagName: "tr",
+
+  /** @property {string|function(): string} */
+  emptyText: null,
+
+  /**
+     Initializer.
+
+     @param {Object} options
+     @param {string|function(): string} options.emptyText
+     @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
+   */
+  initialize: function (options) {
+    this.emptyText = options.emptyText;
+    this.columns =  options.columns;
+  },
+
+  /**
+     Renders an empty row.
+  */
+  render: function () {
+    this.$el.empty();
+
+    var td = document.createElement("td");
+    td.setAttribute("colspan", this.columns.length);
+    td.appendChild(document.createTextNode(_.result(this, "emptyText")));
+
+    this.el.className = "empty";
+    this.el.appendChild(td);
+
+    return this;
+  }
+});
+
+/*
+  backgrid
+  http://github.com/wyuenho/backgrid
+
+  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+  Licensed under the MIT license.
+*/
+
+/**
+   HeaderCell is a special cell class that renders a column header cell. If the
+   column is sortable, a sorter is also rendered and will trigger a table
+   refresh after sorting.
+
+   @class Backgrid.HeaderCell
+   @extends Backbone.View
+ */
+var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
+
+  /** @property */
+  tagName: "th",
+
+  /** @property */
+  events: {
+    "click a": "onClick"
+  },
+
+  /**
+     Initializer.
+
+     @param {Object} options
+     @param {Backgrid.Column|Object} options.column
+
+     @throws {TypeError} If options.column or options.collection is undefined.
+   */
+  initialize: function (options) {
+    this.column = options.column;
+    if (!(this.column instanceof Column)) {
+      this.column = new Column(this.column);
+    }
+
+    var column = this.column, collection = this.collection, $el = this.$el;
+
+    this.listenTo(column, "change:editable change:sortable change:renderable",
+                  function (column) {
+                    var changed = column.changedAttributes();
+                    for (var key in changed) {
+                      if (changed.hasOwnProperty(key)) {
+                        $el.toggleClass(key, changed[key]);
+                      }
+                    }
+                  });
+    this.listenTo(column, "change:direction", this.setCellDirection);
+    this.listenTo(column, "change:name change:label", this.render);
+
+    if (Backgrid.callByNeed(column.editable(), column, collection)) $el.addClass("editable");
+    if (Backgrid.callByNeed(column.sortable(), column, collection)) $el.addClass("sortable");
+    if (Backgrid.callByNeed(column.renderable(), column, collection)) $el.addClass("renderable");
+
+    this.listenTo(collection.fullCollection || collection, "sort", this.removeCellDirection);
+  },
+
+  /**
+     Event handler for the collection's `sort` event. Removes all the CSS
+     direction classes.
+   */
+  removeCellDirection: function () {
+    this.$el.removeClass("ascending").removeClass("descending");
+    this.column.set("direction", null);
+  },
+
+  /**
+     Event handler for the column's `change:direction` event. If this
+     HeaderCell's column is being sorted on, it applies the direction given as a
+     CSS class to the header cell. Removes all the CSS direction classes
+     otherwise.
+   */
+  setCellDirection: function (column, direction) {
+    this.$el.removeClass("ascending").removeClass("descending");
+    if (column.cid == this.column.cid) this.$el.addClass(direction);
+  },
+
+  /**
+     Event handler for the `click` event on the cell's anchor. If the column is
+     sortable, clicking on the anchor will cycle through 3 sorting orderings -
+     `ascending`, `descending`, and default.
+   */
+  onClick: function (e) {
+    e.preventDefault();
+
+    var column = this.column;
+    var collection = this.collection;
+    var event = "backgrid:sort";
+
+    function cycleSort(header, col) {
+      if (column.get("direction") === "ascending") collection.trigger(event, col, "descending");
+      else if (column.get("direction") === "descending") collection.trigger(event, col, null);
+      else collection.trigger(event, col, "ascending");
+    }
+
+    function toggleSort(header, col) {
+      if (column.get("direction") === "ascending") collection.trigger(event, col, "descending");
+      else collection.trigger(event, col, "ascending");
+    }
+
+    var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection);
+    if (sortable) {
+      var sortType = column.get("sortType");
+      if (sortType === "toggle") toggleSort(this, column);
+      else cycleSort(this, column);
+    }
+  },
+
+  /**
+     Renders a header cell with a sorter, a label, and a class name for this
+     column.
+   */
+  render: function () {
+    this.$el.empty();
+    var column = this.column;
+    var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection);
+    var label;
+    if(sortable){
+      label = $("<a>").text(column.get("label")).append("<b class='sort-caret'></b>");
+    } else {
+      label = document.createTextNode(column.get("label"));
+    }
+
+    this.$el.append(label);
+    this.$el.addClass(column.get("name"));
+    this.$el.addClass(column.get("direction"));
+    this.delegateEvents();
+    return this;
+  }
+
+});
+
+/**
+   HeaderRow is a controller for a row of header cells.
+
+   @class Backgrid.HeaderRow
+   @extends Backgrid.Row
+ */
+var HeaderRow = Backgrid.HeaderRow = Backgrid.Row.extend({
+
+  requiredOptions: ["columns", "collection"],
+
+  /**
+     Initializer.
+
+     @param {Object} options
+     @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
+     @param {Backgrid.HeaderCell} [options.headerCell] Customized default
+     HeaderCell for all the columns. Supply a HeaderCell class or instance to a
+     the `headerCell` key in a column definition for column-specific header
+     rendering.
+
+     @throws {TypeError} If options.columns or options.collection is undefined.
+   */
+  initialize: function () {
+    Backgrid.Row.prototype.initialize.apply(this, arguments);
+  },
+
+  makeCell: function (column, options) {
+    var headerCell = column.get("headerCell") || options.headerCell || HeaderCell;
+    headerCell = new headerCell({
+      column: column,
+      collection: this.collection
+    });
+    return headerCell;
+  }
+
+});
+
+/**
+   Header is a special structural view class that renders a table head with a
+   single row of header cells.
+
+   @class Backgrid.Header
+   @extends Backbone.View
+ */
+var Header = Backgrid.Header = Backbone.View.extend({
+
+  /** @property */
+  tagName: "thead",
+
+  /**
+     Initializer. Initializes this table head view to contain a single header
+     row view.
+
+     @param {Object} options
+     @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
+     @param {Backbone.Model} options.model The model instance to render.
+
+     @throws {TypeError} If options.columns or options.model is undefined.
+   */
+  initialize: function (options) {
+    this.columns = options.columns;
+    if (!(this.columns instanceof Backbone.Collection)) {
+      this.columns = new Columns(this.columns);
+    }
+
+    this.row = new Backgrid.HeaderRow({
+      columns: this.columns,
+      collection: this.collection
+    });
+  },
+
+  /**
+     Renders this table head with a single row of header cells.
+   */
+  render: function () {
+    this.$el.append(this.row.render().$el);
+    this.delegateEvents();
+    return this;
+  },
+
+  /**
+     Clean up this header and its row.
+
+     @chainable
+   */
+  remove: function () {
+    this.row.remove.apply(this.row, arguments);
+    return Backbone.View.prototype.remove.apply(this, arguments);
+  }
+
+});
+
+/*
+  backgrid
+  http://github.com/wyuenho/backgrid
+
+  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+  Licensed under the MIT license.
+*/
+
+/**
+   Body is the table body which contains the rows inside a table. Body is
+   responsible for refreshing the rows after sorting, insertion and removal.
+
+   @class Backgrid.Body
+   @extends Backbone.View
+*/
+var Body = Backgrid.Body = Backbone.View.extend({
+
+  /** @property */
+  tagName: "tbody",
+
+  /**
+     Initializer.
+
+     @param {Object} options
+     @param {Backbone.Collection} options.collection
+     @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
+     Column metadata.
+     @param {Backgrid.Row} [options.row=Backgrid.Row] The Row class to use.
+     @param {string|function(): string} [options.emptyText] The text to display in the empty row.
+
+     @throws {TypeError} If options.columns or options.collection is undefined.
+
+     See Backgrid.Row.
+  */
+  initialize: function (options) {
+
+    this.columns = options.columns;
+    if (!(this.columns instanceof Backbone.Collection)) {
+      this.columns = new Columns(this.columns);
+    }
+
+    this.row = options.row || Row;
+    this.rows = this.collection.map(function (model) {
+      var row = new this.row({
+        columns: this.columns,
+        model: model
+      });
+
+      return row;
+    }, this);
+
+    this.emptyText = options.emptyText;
+    this._unshiftEmptyRowMayBe();
+
+    var collection = this.collection;
+    this.listenTo(collection, "add", this.insertRow);
+    this.listenTo(collection, "remove", this.removeRow);
+    this.listenTo(collection, "sort", this.refresh);
+    this.listenTo(collection, "reset", this.refresh);
+    this.listenTo(collection, "backgrid:sort", this.sort);
+    this.listenTo(collection, "backgrid:edited", this.moveToNextCell);
+  },
+
+  _unshiftEmptyRowMayBe: function () {
+    if (this.rows.length === 0 && this.emptyText != null) {
+      this.rows.unshift(new EmptyRow({
+        emptyText: this.emptyText,
+        columns: this.columns
+      }));
+    }
+  },
+
+  /**
+     This method can be called either directly or as a callback to a
+     [Backbone.Collecton#add](http://backbonejs.org/#Collection-add) event.
+
+     When called directly, it accepts a model or an array of models and an
+     option hash just like
+     [Backbone.Collection#add](http://backbonejs.org/#Collection-add) and
+     delegates to it. Once the model is added, a new row is inserted into the
+     body and automatically rendered.
+
+     When called as a callback of an `add` event, splices a new row into the
+     body and renders it.
+
+     @param {Backbone.Model} model The model to render as a row.
+     @param {Backbone.Collection} collection When called directly, this
+     parameter is actually the options to
+     [Backbone.Collection#add](http://backbonejs.org/#Collection-add).
+     @param {Object} options When called directly, this must be null.
+
+     See:
+
+     - [Backbone.Collection#add](http://backbonejs.org/#Collection-add)
+  */
+  insertRow: function (model, collection, options) {
+
+    if (this.rows[0] instanceof EmptyRow) this.rows.pop().remove();
+
+    // insertRow() is called directly
+    if (!(collection instanceof Backbone.Collection) && !options) {
+      this.collection.add(model, (options = collection));
+      return;
+    }
+
+    var row = new this.row({
+      columns: this.columns,
+      model: model
+    });
+
+    var index = collection.indexOf(model);
+    this.rows.splice(index, 0, row);
+
+    var $el = this.$el;
+    var $children = $el.children();
+    var $rowEl = row.render().$el;
+
+    if (index >= $children.length) {
+      $el.append($rowEl);
+    }
+    else {
+      $children.eq(index).before($rowEl);
+    }
+
+    return this;
+  },
+
+  /**
+     The method can be called either directly or as a callback to a
+     [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove)
+     event.
+
+     When called directly, it accepts a model or an array of models and an
+     option hash just like
+     [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove) and
+     delegates to it. Once the model is removed, a corresponding row is removed
+     from the body.
+
+     When called as a callback of a `remove` event, splices into the rows and
+     removes the row responsible for rendering the model.
+
+     @param {Backbone.Model} model The model to remove from the body.
+     @param {Backbone.Collection} collection When called directly, this
+     parameter is actually the options to
+     [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove).
+     @param {Object} options When called directly, this must be null.
+
+     See:
+
+     - [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove)
+  */
+  removeRow: function (model, collection, options) {
+
+    // removeRow() is called directly
+    if (!options) {
+      this.collection.remove(model, (options = collection));
+      this._unshiftEmptyRowMayBe();
+      return;
+    }
+
+    if (_.isUndefined(options.render) || options.render) {
+      this.rows[options.index].remove();
+    }
+
+    this.rows.splice(options.index, 1);
+    this._unshiftEmptyRowMayBe();
+
+    return this;
+  },
+
+  /**
+     Reinitialize all the rows inside the body and re-render them. Triggers a
+     Backbone `backgrid:refresh` event from the collection along with the body
+     instance as its sole parameter when done.
+  */
+  refresh: function () {
+    for (var i = 0; i < this.rows.length; i++) {
+      this.rows[i].remove();
+    }
+
+    this.rows = this.collection.map(function (model) {
+      var row = new this.row({
+        columns: this.columns,
+        model: model
+      });
+
+      return row;
+    }, this);
+    this._unshiftEmptyRowMayBe();
+
+    this.render();
+
+    this.collection.trigger("backgrid:refresh", this);
+
+    return this;
+  },
+
+  /**
+     Renders all the rows inside this body. If the collection is empty and
+     `options.emptyText` is defined and not null in the constructor, an empty
+     row is rendered, otherwise no row is rendered.
+  */
+  render: function () {
+    this.$el.empty();
+
+    var fragment = document.createDocumentFragment();
+    for (var i = 0; i < this.rows.length; i++) {
+      var row = this.rows[i];
+      fragment.appendChild(row.render().el);
+    }
+
+    this.el.appendChild(fragment);
+
+    this.delegateEvents();
+
+    return this;
+  },
+
+  /**
+     Clean up this body and it's rows.
+
+     @chainable
+  */
+  remove: function () {
+    for (var i = 0; i < this.rows.length; i++) {
+      var row = this.rows[i];
+      row.remove.apply(row, arguments);
+    }
+    return Backbone.View.prototype.remove.apply(this, arguments);
+  },
+
+  /**
+     If the underlying collection is a Backbone.PageableCollection in
+     server-mode or infinite-mode, a page of models is fetched after sorting is
+     done on the server.
+
+     If the underlying collection is a Backbone.PageableCollection in
+     client-mode, or any
+     [Backbone.Collection](http://backbonejs.org/#Collection) instance, sorting
+     is done on the client side. If the collection is an instance of a
+     Backbone.PageableCollection, sorting will be done globally on all the pages
+     and the current page will then be returned.
+
+     Triggers a Backbone `backgrid:sorted` event from the collection when done
+     with the column, direction and a reference to the collection.
+
+     @param {Backgrid.Column} column
+     @param {null|"ascending"|"descending"} direction
+
+     See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator)
+  */
+  sort: function (column, direction) {
+
+    if (!_.contains(["ascending", "descending", null], direction)) {
+      throw new RangeError('direction must be one of "ascending", "descending" or `null`');
+    }
+
+    if (_.isString(column)) column = this.columns.findWhere({name: column});
+
+    var collection = this.collection;
+
+    var order;
+    if (direction === "ascending") order = -1;
+    else if (direction === "descending") order = 1;
+    else order = null;
+
+    var comparator = this.makeComparator(column.get("name"), order,
+                                         order ?
+                                         column.sortValue() :
+                                         function (model) {
+                                           return model.cid.replace('c', '') * 1;
+                                         });
+
+    if (Backbone.PageableCollection &&
+        collection instanceof Backbone.PageableCollection) {
+
+      collection.setSorting(order && column.get("name"), order,
+                            {sortValue: column.sortValue()});
+
+      if (collection.fullCollection) {
+        // If order is null, pageable will remove the comparator on both sides,
+        // in this case the default insertion order comparator needs to be
+        // attached to get back to the order before sorting.
+        if (collection.fullCollection.comparator == null) {
+          collection.fullCollection.comparator = comparator;
+        }
+        collection.fullCollection.sort();
+        collection.trigger("backgrid:sorted", column, direction, collection);
+      }
+      else collection.fetch({reset: true, success: function () {
+        collection.trigger("backgrid:sorted", column, direction, collection);
+      }});
+    }
+    else {
+      collection.comparator = comparator;
+      collection.sort();
+      collection.trigger("backgrid:sorted", column, direction, collection);
+    }
+
+    column.set("direction", direction);
+
+    return this;
+  },
+
+  makeComparator: function (attr, order, func) {
+
+    return function (left, right) {
+      // extract the values from the models
+      var l = func(left, attr), r = func(right, attr), t;
+
+      // if descending order, swap left and right
+      if (order === 1) t = l, l = r, r = t;
+
+      // compare as usual
+      if (l === r) return 0;
+      else if (l < r) return -1;
+      return 1;
+    };
+  },
+
+  /**
+     Moves focus to the next renderable and editable cell and return the
+     currently editing cell to display mode.
+
+     Triggers a `backgrid:next` event on the model with the indices of the row
+     and column the user *intended* to move to, and whether the intended move
+     was going to go out of bounds. Note that *out of bound* always means an
+     attempt to go past the end of the last row.
+
+     @param {Backbone.Model} model The originating model
+     @param {Backgrid.Column} column The originating model column
+     @param {Backgrid.Command} command The Command object constructed from a DOM
+     event
+  */
+  moveToNextCell: function (model, column, command) {
+    var i = this.collection.indexOf(model);
+    var j = this.columns.indexOf(column);
+    var cell, renderable, editable, m, n;
+
+    this.rows[i].cells[j].exitEditMode();
+
+    if (command.moveUp() || command.moveDown() || command.moveLeft() ||
+        command.moveRight() || command.save()) {
+      var l = this.columns.length;
+      var maxOffset = l * this.collection.length;
+
+      if (command.moveUp() || command.moveDown()) {
+        m = i + (command.moveUp() ? -1 : 1);
+        var row = this.rows[m];
+        if (row) {
+          cell = row.cells[j];
+          if (Backgrid.callByNeed(cell.column.editable(), cell.column, model)) {
+            cell.enterEditMode();
+            model.trigger("backgrid:next", m, j, false);
+          }
+        }
+        else model.trigger("backgrid:next", m, j, true);
+      }
+      else if (command.moveLeft() || command.moveRight()) {
+        var right = command.moveRight();
+        for (var offset = i * l + j + (right ? 1 : -1);
+             offset >= 0 && offset < maxOffset;
+             right ? offset++ : offset--) {
+          m = ~~(offset / l);
+          n = offset - m * l;
+          cell = this.rows[m].cells[n];
+          renderable = Backgrid.callByNeed(cell.column.renderable(), cell.column, cell.model);
+          editable = Backgrid.callByNeed(cell.column.editable(), cell.column, model);
+          if (renderable && editable) {
+            cell.enterEditMode();
+            model.trigger("backgrid:next", m, n, false);
+            break;
+          }
+        }
+
+        if (offset == maxOffset) {
+          model.trigger("backgrid:next", ~~(offset / l), offset - m * l, true);
+        }
+      }
+    }
+
+    return this;
+  }
+});
+
+/*
+  backgrid
+  http://github.com/wyuenho/backgrid
+
+  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+  Licensed under the MIT license.
+*/
+
+/**
+   A Footer is a generic class that only defines a default tag `tfoot` and
+   number of required parameters in the initializer.
+
+   @abstract
+   @class Backgrid.Footer
+   @extends Backbone.View
+ */
+var Footer = Backgrid.Footer = Backbone.View.extend({
+
+  /** @property */
+  tagName: "tfoot",
+
+  /**
+     Initializer.
+
+     @param {Object} options
+     @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
+     Column metadata.
+     @param {Backbone.Collection} options.collection
+
+     @throws {TypeError} If options.columns or options.collection is undefined.
+  */
+  initialize: function (options) {
+    this.columns = options.columns;
+    if (!(this.columns instanceof Backbone.Collection)) {
+      this.columns = new Backgrid.Columns(this.columns);
+    }
+  }
+
+});
+
+/*
+  backgrid
+  http://github.com/wyuenho/backgrid
+
+  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+  Licensed under the MIT license.
+*/
+
+/**
+   Grid represents a data grid that has a header, body and an optional footer.
+
+   By default, a Grid treats each model in a collection as a row, and each
+   attribute in a model as a column. To render a grid you must provide a list of
+   column metadata and a collection to the Grid constructor. Just like any
+   Backbone.View class, the grid is rendered as a DOM node fragment when you
+   call render().
+
+       var grid = Backgrid.Grid({
+         columns: [{ name: "id", label: "ID", type: "string" },
+          // ...
+         ],
+         collections: books
+       });
+
+       $("#table-container").append(grid.render().el);
+
+   Optionally, if you want to customize the rendering of the grid's header and
+   footer, you may choose to extend Backgrid.Header and Backgrid.Footer, and
+   then supply that class or an instance of that class to the Grid constructor.
+   See the documentation for Header and Footer for further details.
+
+       var grid = Backgrid.Grid({
+         columns: [{ name: "id", label: "ID", type: "string" }],
+         collections: books,
+         header: Backgrid.Header.extend({
+              //...
+         }),
+         footer: Backgrid.Paginator
+       });
+
+   Finally, if you want to override how the rows are rendered in the table body,
+   you can supply a Body subclass as the `body` attribute that uses a different
+   Row class.
+
+   @class Backgrid.Grid
+   @extends Backbone.View
+
+   See:
+
+   - Backgrid.Column
+   - Backgrid.Header
+   - Backgrid.Body
+   - Backgrid.Row
+   - Backgrid.Footer
+*/
+var Grid = Backgrid.Grid = Backbone.View.extend({
+
+  /** @property */
+  tagName: "table",
+
+  /** @property */
+  className: "backgrid",
+
+  /** @property */
+  header: Header,
+
+  /** @property */
+  body: Body,
+
+  /** @property */
+  footer: null,
+
+  /**
+     Initializes a Grid instance.
+
+     @param {Object} options
+     @param {Backbone.Collection.<Backgrid.Columns>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
+     @param {Backbone.Collection} options.collection The collection of tabular model data to display.
+     @param {Backgrid.Header} [options.header=Backgrid.Header] An optional Header class to override the default.
+     @param {Backgrid.Body} [options.body=Backgrid.Body] An optional Body class to override the default.
+     @param {Backgrid.Row} [options.row=Backgrid.Row] An optional Row class to override the default.
+     @param {Backgrid.Footer} [options.footer=Backgrid.Footer] An optional Footer class.
+   */
+  initialize: function (options) {
+    // Convert the list of column objects here first so the subviews don't have
+    // to.
+    if (!(options.columns instanceof Backbone.Collection)) {
+      options.columns = new Columns(options.columns);
+    }
+    this.columns = options.columns;
+
+    var filteredOptions = _.omit(options, ["el", "id", "attributes",
+                                           "className", "tagName", "events"]);
+
+    // must construct body first so it listens to backgrid:sort first
+    this.body = options.body || this.body;
+    this.body = new this.body(filteredOptions);
+
+    this.header = options.header || this.header;
+    if (this.header) {
+      this.header = new this.header(filteredOptions);
+    }
+
+    this.footer = options.footer || this.footer;
+    if (this.footer) {
+      this.footer = new this.footer(filteredOptions);
+    }
+
+    this.listenTo(this.columns, "reset", function () {
+      if (this.header) {
+        this.header = new (this.header.remove().constructor)(filteredOptions);
+      }
+      this.body = new (this.body.remove().constructor)(filteredOptions);
+      if (this.footer) {
+        this.footer = new (this.footer.remove().constructor)(filteredOptions);
+      }
+      this.render();
+    });
+  },
+
+  /**
+     Delegates to Backgrid.Body#insertRow.
+   */
+  insertRow: function () {
+    this.body.insertRow.apply(this.body, arguments);
+    return this;
+  },
+
+  /**
+     Delegates to Backgrid.Body#removeRow.
+   */
+  removeRow: function () {
+    this.body.removeRow.apply(this.body, arguments);
+    return this;
+  },
+
+  /**
+     Delegates to Backgrid.Columns#add for adding a column. Subviews can listen
+     to the `add` event from their internal `columns` if rerendering needs to
+     happen.
+
+     @param {Object} [options] Options for `Backgrid.Columns#add`.
+   */
+  insertColumn: function () {
+    this.columns.add.apply(this.columns, arguments);
+    return this;
+  },
+
+  /**
+     Delegates to Backgrid.Columns#remove for removing a column. Subviews can
+     listen to the `remove` event from the internal `columns` if rerendering
+     needs to happen.
+
+     @param {Object} [options] Options for `Backgrid.Columns#remove`.
+   */
+  removeColumn: function () {
+    this.columns.remove.apply(this.columns, arguments);
+    return this;
+  },
+
+  /**
+     Delegates to Backgrid.Body#sort.
+   */
+  sort: function () {
+    this.body.sort.apply(this.body, arguments);
+    return this;
+  },
+
+  /**
+     Renders the grid's header, then footer, then finally the body. Triggers a
+     Backbone `backgrid:rendered` event along with a reference to the grid when
+     the it has successfully been rendered.
+   */
+  render: function () {
+    this.$el.empty();
+
+    if (this.header) {
+      this.$el.append(this.header.render().$el);
+    }
+
+    if (this.footer) {
+      this.$el.append(this.footer.render().$el);
+    }
+
+    this.$el.append(this.body.render().$el);
+
+    this.delegateEvents();
+
+    this.trigger("backgrid:rendered", this);
+
+    return this;
+  },
+
+  /**
+     Clean up this grid and its subviews.
+
+     @chainable
+   */
+  remove: function () {
+    this.header && this.header.remove.apply(this.header, arguments);
+    this.body.remove.apply(this.body, arguments);
+    this.footer && this.footer.remove.apply(this.footer, arguments);
+    return Backbone.View.prototype.remove.apply(this, arguments);
+  }
+
+});
+return Backgrid;
+}));
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/backgrid-0.3.5.min.js b/htrace-core/src/web/lib/js/backgrid-0.3.5.min.js
deleted file mode 100644
index 4938964..0000000
--- a/htrace-core/src/web/lib/js/backgrid-0.3.5.min.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/*!
-  backgrid
-  http://github.com/wyuenho/backgrid
-
-  Copyright (c) 2014 Jimmy Yuen Ho Wong and contributors <wyuenho@gmail.com>
-  Licensed under the MIT license.
-*/
-!function(a){"object"==typeof exports?module.exports=a(module.exports,require("underscore"),require("backbone")):a(this,this._,this.Backbone)}(function(a,b,c){"use strict";function d(a,b,c){var d=b-(a+"").length;d=0>d?0:d;for(var e="",f=0;d>f;f++)e+=c;return e+a}var e="	\n\f\r   ᠎              \u2028\u2029";if(!String.prototype.trim||e.trim()){e="["+e+"]";var f=new RegExp("^"+e+e+"*"),g=new RegExp(e+e+"*$");String.prototype.trim=function(){if(void 0===this||null===this)throw new TypeError("can't convert "+this+" to object");return String(this).replace(f,"").replace(g,"")}}var h=c.$,i=a.Backgrid={Extension:{},resolveNameToClass:function(a,c){if(b.isString(a)){var d=b.map(a.split("-"),function(a){return a.slice(0,1).toUpperCase()+a.slice(1)}).join("")+c,e=i[d]||i.Extension[d];if(b.isUndefined(e))throw new ReferenceError("Class '"+d+"' not found");return e}return a},callByNeed:function(){var a=arguments[0];if(!b.isFunction(a))return a;var c=arguments[1],d=[].slice.call(arguments,2);return a.apply(c,d+""?d:[])}};b.extend(i,c.Events);var j=i.Command=function(a){b.extend(this,{altKey:!!a.altKey,"char":a["char"],charCode:a.charCode,ctrlKey:!!a.ctrlKey,key:a.key,keyCode:a.keyCode,locale:a.locale,location:a.location,metaKey:!!a.metaKey,repeat:!!a.repeat,shiftKey:!!a.shiftKey,which:a.which})};b.extend(j.prototype,{moveUp:function(){return 38==this.keyCode},moveDown:function(){return 40===this.keyCode},moveLeft:function(){return this.shiftKey&&9===this.keyCode},moveRight:function(){return!this.shiftKey&&9===this.keyCode},save:function(){return 13===this.keyCode},cancel:function(){return 27===this.keyCode},passThru:function(){return!(this.moveUp()||this.moveDown()||this.moveLeft()||this.moveRight()||this.save()||this.cancel())}});var k=i.CellFormatter=function(){};b.extend(k.prototype,{fromRaw:function(a){return a},toRaw:function(a){return a}});var l=i.NumberFormatter=function(a){if(b.extend(this,this.defaults,a||{}),this.decimals<0||this.decimals>20)throw new RangeError("decimals must be between 0 and 20")};l.prototype=new k,b.extend(l.prototype,{defaults:{decimals:2,decimalSeparator:".",orderSeparator:","},HUMANIZED_NUM_RE:/(\d)(?=(?:\d{3})+$)/g,fromRaw:function(a){if(b.isNull(a)||b.isUndefined(a))return"";a=a.toFixed(~~this.decimals);var c=a.split("."),d=c[0],e=c[1]?(this.decimalSeparator||".")+c[1]:"";return d.replace(this.HUMANIZED_NUM_RE,"$1"+this.orderSeparator)+e},toRaw:function(a){if(a=a.trim(),""===a)return null;for(var c="",d=a.split(this.orderSeparator),e=0;e<d.length;e++)c+=d[e];var f=c.split(this.decimalSeparator);c="";for(var e=0;e<f.length;e++)c=c+f[e]+".";"."===c[c.length-1]&&(c=c.slice(0,c.length-1));var g=1*(1*c).toFixed(~~this.decimals);return b.isNumber(g)&&!b.isNaN(g)?g:void 0}});var m=i.PercentFormatter=function(){i.NumberFormatter.apply(this,arguments)};m.prototype=new i.NumberFormatter,b.extend(m.prototype,{defaults:b.extend({},l.prototype.defaults,{multiplier:1,symbol:"%"}),fromRaw:function(a){var b=[].slice.call(arguments,1);return b.unshift(a*this.multiplier),(l.prototype.fromRaw.apply(this,b)||"0")+this.symbol},toRaw:function(a){var c=a.split(this.symbol);if(c&&c[0]&&""===c[1]||null==c[1]){var d=l.prototype.toRaw.call(this,c[0]);return b.isUndefined(d)?d:d/this.multiplier}}});var n=i.DatetimeFormatter=function(a){if(b.extend(this,this.defaults,a||{}),!this.includeDate&&!this.includeTime)throw new Error("Either includeDate or includeTime must be true")};n.prototype=new k,b.extend(n.prototype,{defaults:{includeDate:!0,includeTime:!0,includeMilli:!1},DATE_RE:/^([+\-]?\d{4})-(\d{2})-(\d{2})$/,TIME_RE:/^(\d{2}):(\d{2}):(\d{2})(\.(\d{3}))?$/,ISO_SPLITTER_RE:/T|Z| +/,_convert:function(a,c){if(""===(a+"").trim())return null;var e,f=null;if(b.isNumber(a)){var g=new Date(a);e=d(g.getUTCFullYear(),4,0)+"-"+d(g.getUTCMonth()+1,2,0)+"-"+d(g.getUTCDate(),2,0),f=d(g.getUTCHours(),2,0)+":"+d(g.getUTCMinutes(),2,0)+":"+d(g.getUTCSeconds(),2,0)}else{a=a.trim();var h=a.split(this.ISO_SPLITTER_RE)||[];e=this.DATE_RE.test(h[0])?h[0]:"",f=e&&h[1]?h[1]:this.TIME_RE.test(h[0])?h[0]:""}var i=this.DATE_RE.exec(e)||[],j=this.TIME_RE.exec(f)||[];if(c){if(this.includeDate&&b.isUndefined(i[0]))return;if(this.includeTime&&b.isUndefined(j[0]))return;if(!this.includeDate&&e)return;if(!this.includeTime&&f)return}var g=new Date(Date.UTC(1*i[1]||0,1*i[2]-1||0,1*i[3]||0,1*j[1]||null,1*j[2]||null,1*j[3]||null,1*j[5]||null)),k="";return this.includeDate&&(k=d(g.getUTCFullYear(),4,0)+"-"+d(g.getUTCMonth()+1,2,0)+"-"+d(g.getUTCDate(),2,0)),this.includeTime&&(k=k+(this.includeDate?"T":"")+d(g.getUTCHours(),2,0)+":"+d(g.getUTCMinutes(),2,0)+":"+d(g.getUTCSeconds(),2,0),this.includeMilli&&(k=k+"."+d(g.getUTCMilliseconds(),3,0))),this.includeDate&&this.includeTime&&(k+="Z"),k},fromRaw:function(a){return b.isNull(a)||b.isUndefined(a)?"":this._convert(a)},toRaw:function(a){return this._convert(a,!0)}});var o=i.StringFormatter=function(){};o.prototype=new k,b.extend(o.prototype,{fromRaw:function(a){return b.isUndefined(a)||b.isNull(a)?"":a+""}});var p=i.EmailFormatter=function(){};p.prototype=new k,b.extend(p.prototype,{toRaw:function(a){var c=a.trim().split("@");return 2===c.length&&b.all(c)?a:void 0}});var q=i.SelectFormatter=function(){};q.prototype=new k,b.extend(q.prototype,{fromRaw:function(a){return b.isArray(a)?a:null!=a?[a]:[]}});var r=i.CellEditor=c.View.extend({initialize:function(a){this.formatter=a.formatter,this.column=a.column,this.column instanceof C||(this.column=new C(this.column)),this.listenTo(this.model,"backgrid:editing",this.postRender)},postRender:function(a,b){return(null==b||b.get("name")==this.column.get("name"))&&this.$el.focus(),this}}),s=i.InputCellEditor=r.extend({tagName:"input",attributes:{type:"text"},events:{blur:"saveOrCancel",keydown:"saveOrCancel"},initialize:function(a){s.__super__.initialize.apply(this,arguments),a.placeholder&&this.$el.attr("placeholder",a.placeholder)},render:function(){var a=this.model;return this.$el.val(this.formatter.fromRaw(a.get(this.column.get("name")),a)),this},saveOrCancel:function(a){var c=this.formatter,d=this.model,e=this.column,f=new j(a),g="blur"===a.type;if(f.moveUp()||f.moveDown()||f.moveLeft()||f.moveRight()||f.save()||g){a.preventDefault(),a.stopPropagation();var h=this.$el.val(),i=c.toRaw(h,d);b.isUndefined(i)?d.trigger("backgrid:error",d,e,h):(d.set(e.get("name"),i),d.trigger("backgrid:edited",d,e,f))}else f.cancel()&&(a.stopPropagation(),d.trigger("backgrid:edited",d,e,f))},postRender:function(a,b){if(null==b||b.get("name")==this.column.get("name"))if("right"===this.$el.css("text-align")){var c=this.$el.val();this.$el.focus().val(null).val(c)}else this.$el.focus();return this}}),t=i.Cell=c.View.extend({tagName:"td",formatter:k,editor:s,events:{click:"enterEditMode"},initialize:function(a){this.column=a.column,this.column instanceof C||(this.column=new C(this.column));var c=this.column,d=this.model,e=this.$el,f=i.resolveNameToClass(c.get("formatter")||this.formatter,"Formatter");b.isFunction(f.fromRaw)||b.isFunction(f.toRaw)||(f=new f),this.formatter=f,this.editor=i.resolveNameToClass(this.editor,"CellEditor"),this.listenTo(d,"change:"+c.get("name"),function(){e.hasClass("editor")||this.render()}),this.listenTo(d,"backgrid:error",this.renderError),this.listenTo(c,"change:editable change:sortable change:renderable",function(a){var b=a.changedAttributes();for(var c in b)b.hasOwnProperty(c)&&e.toggleClass(c,b[c])}),i.callByNeed(c.editable(),c,d)&&e.addClass("editable"),i.callByNeed(c.sortable(),c,d)&&e.addClass("sortable"),i.callByNeed(c.renderable(),c,d)&&e.addClass("renderable")},render:function(){this.$el.empty();var a=this.model;return this.$el.text(this.formatter.fromRaw(a.get(this.column.get("name")),a)),this.delegateEvents(),this},enterEditMode:function(){var a=this.model,b=this.column,c=i.callByNeed(b.editable(),b,a);c&&(this.currentEditor=new this.editor({column:this.column,model:this.model,formatter:this.formatter}),a.trigger("backgrid:edit",a,b,this,this.currentEditor),this.undelegateEvents(),this.$el.empty(),this.$el.append(this.currentEditor.$el),this.currentEditor.render(),this.$el.addClass("editor"),a.trigger("backgrid:editing",a,b,this,this.currentEditor))},renderError:function(a,b){(null==b||b.get("name")==this.column.get("name"))&&this.$el.addClass("error")},exitEditMode:function(){this.$el.removeClass("error"),this.currentEditor.remove(),this.stopListening(this.currentEditor),delete this.currentEditor,this.$el.removeClass("editor"),this.render()},remove:function(){return this.currentEditor&&(this.currentEditor.remove.apply(this.currentEditor,arguments),delete this.currentEditor),t.__super__.remove.apply(this,arguments)}}),u=i.StringCell=t.extend({className:"string-cell",formatter:o}),v=i.UriCell=t.extend({className:"uri-cell",title:null,target:"_blank",initialize:function(a){v.__super__.initialize.apply(this,arguments),this.title=a.title||this.title,this.target=a.target||this.target},render:function(){this.$el.empty();var a=this.model.get(this.column.get("name")),b=this.formatter.fromRaw(a,this.model);return this.$el.append(h("<a>",{tabIndex:-1,href:a,title:this.title||b,target:this.target}).text(b)),this.delegateEvents(),this}}),w=(i.EmailCell=u.extend({className:"email-cell",formatter:p,render:function(){this.$el.empty();var a=this.model,b=this.formatter.fromRaw(a.get(this.column.get("name")),a);return this.$el.append(h("<a>",{tabIndex:-1,href:"mailto:"+b,title:b}).text(b)),this.delegateEvents(),this}}),i.NumberCell=t.extend({className:"number-cell",decimals:l.prototype.defaults.decimals,decimalSeparator:l.prototype.defaults.decimalSeparator,orderSeparator:l.prototype.defaults.orderSeparator,formatter:l,initialize:function(){w.__super__.initialize.apply(this,arguments);var a=this.formatter;a.decimals=this.decimals,a.decimalSeparator=this.decimalSeparator,a.orderSeparator=this.orderSeparator}})),x=(i.IntegerCell=w.extend({className:"integer-cell",decimals:0}),i.PercentCell=w.extend({className:"percent-cell",multiplier:m.prototype.defaults.multiplier,symbol:m.prototype.defaults.symbol,formatter:m,initialize:function(){x.__super__.initialize.apply(this,arguments);var a=this.formatter;a.multiplier=this.multiplier,a.symbol=this.symbol}})),y=i.DatetimeCell=t.extend({className:"datetime-cell",includeDate:n.prototype.defaults.includeDate,includeTime:n.prototype.defaults.includeTime,includeMilli:n.prototype.defaults.includeMilli,formatter:n,initialize:function(){y.__super__.initialize.apply(this,arguments);var a=this.formatter;a.includeDate=this.includeDate,a.includeTime=this.includeTime,a.includeMilli=this.includeMilli;var c=this.includeDate?"YYYY-MM-DD":"";c+=this.includeDate&&this.includeTime?"T":"",c+=this.includeTime?"HH:mm:ss":"",c+=this.includeTime&&this.includeMilli?".SSS":"",this.editor=this.editor.extend({attributes:b.extend({},this.editor.prototype.attributes,this.editor.attributes,{placeholder:c})})}}),z=(i.DateCell=y.extend({className:"date-cell",includeTime:!1}),i.TimeCell=y.extend({className:"time-cell",includeDate:!1}),i.BooleanCellEditor=r.extend({tagName:"input",attributes:{tabIndex:-1,type:"checkbox"},events:{mousedown:function(){this.mouseDown=!0},blur:"enterOrExitEditMode",mouseup:function(){this.mouseDown=!1},change:"saveOrCancel",keydown:"saveOrCancel"},render:function(){var a=this.model,b=this.formatter.fromRaw(a.get(this.column.get("name")),a);return this.$el.prop("checked",b),this},enterOrExitEditMode:function(a){if(!this.mouseDown){var b=this.model;b.trigger("backgrid:edited",b,this.column,new j(a))}},saveOrCancel:function(a){var b=this.model,c=this.column,d=this.formatter,e=new j(a);if(e.passThru()&&"change"!=a.type)return!0;e.cancel()&&(a.stopPropagation(),b.trigger("backgrid:edited",b,c,e));var f=this.$el;if(e.save()||e.moveLeft()||e.moveRight()||e.moveUp()||e.moveDown()){a.preventDefault(),a.stopPropagation();var g=d.toRaw(f.prop("checked"),b);b.set(c.get("name"),g),b.trigger("backgrid:edited",b,c,e)}else if("change"==a.type){var g=d.toRaw(f.prop("checked"),b);b.set(c.get("name"),g),f.focus()}}})),A=(i.BooleanCell=t.extend({className:"boolean-cell",editor:z,events:{click:"enterEditMode"},render:function(){this.$el.empty();var a=this.model,b=this.column,c=i.callByNeed(b.editable(),b,a);return this.$el.append(h("<input>",{tabIndex:-1,type:"checkbox",checked:this.formatter.fromRaw(a.get(b.get("name")),a),disabled:!c})),this.delegateEvents(),this}}),i.SelectCellEditor=r.extend({tagName:"select",events:{change:"save",blur:"close",keydown:"close"},template:b.template('<option value="<%- value %>" <%= selected ? \'selected="selected"\' : "" %>><%- text %></option>',null,{variable:null}),setOptionValues:function(a){this.optionValues=a,this.optionValues=b.result(this,"optionValues")},setMultiple:function(a){this.multiple=a,this.$el.prop("multiple",a)},_renderOptions:function(a,c){for(var d="",e=0;e<a.length;e++)d+=this.template({text:a[e][0],value:a[e][1],selected:b.indexOf(c,a[e][1])>-1});return d},render:function(){this.$el.empty();var a=b.result(this,"optionValues"),c=this.model,d=this.formatter.fromRaw(c.get(this.column.get("name")),c);if(!b.isArray(a))throw new TypeError("optionValues must be an array");for(var e=null,f=null,e=null,g=null,i=null,j=0;j<a.length;j++){var e=a[j];if(b.isArray(e))f=e[0],e=e[1],this.$el.append(this.template({text:f,value:e,selected:b.indexOf(d,e)>-1}));else{if(!b.isObject(e))throw new TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }");g=e.name,i=h("<optgroup></optgroup>",{label:g}),i.append(this._renderOptions.call(this,e.values,d)),this.$el.append(i)}}return this.delegateEvents(),this},save:function(){var a=this.model,b=this.column;a.set(b.get("name"),this.formatter.toRaw(this.$el.val(),a))},close:function(a){var b=this.model,c=this.column,d=new j(a);d.cancel()?(a.stopPropagation(),b.trigger("backgrid:edited",b,c,new j(a))):(d.save()||d.moveLeft()||d.moveRight()||d.moveUp()||d.moveDown()||"blur"==a.type)&&(a.preventDefault(),a.stopPropagation(),this.save(a),b.trigger("backgrid:edited",b,c,new j(a)))}})),B=i.SelectCell=t.extend({className:"select-cell",editor:A,multiple:!1,formatter:q,optionValues:void 0,delimiter:", ",initialize:function(){B.__super__.initialize.apply(this,arguments),this.listenTo(this.model,"backgrid:edit",function(a,b,c,d){b.get("name")==this.column.get("name")&&(d.setOptionValues(this.optionValues),d.setMultiple(this.multiple))})},render:function(){this.$el.empty();var a=b.result(this,"optionValues"),c=this.model,d=this.formatter.fromRaw(c.get(this.column.get("name")),c),e=[];try{if(!b.isArray(a)||b.isEmpty(a))throw new TypeError;for(var f=0;f<d.length;f++)for(var g=d[f],h=0;h<a.length;h++){var i=a[h];if(b.isArray(i)){var j=i[0],i=i[1];i==g&&e.push(j)}else{if(!b.isObject(i))throw new TypeError;for(var k=i.values,l=0;l<k.length;l++){var m=k[l];m[1]==g&&e.push(m[0])}}}this.$el.append(e.join(this.delimiter))}catch(n){if(n instanceof TypeError)throw new TypeError("'optionValues' must be of type {Array.<Array>|Array.<{name: string, values: Array.<Array>}>}");throw n}return this.delegateEvents(),this}}),C=i.Column=c.Model.extend({defaults:{name:void 0,label:void 0,sortable:!0,editable:!0,renderable:!0,formatter:void 0,sortType:"cycle",sortValue:void 0,direction:null,cell:void 0,headerCell:void 0},initialize:function(){this.has("label")||this.set({label:this.get("name")},{silent:!0});var a=i.resolveNameToClass(this.get("headerCell"),"HeaderCell"),b=i.resolveNameToClass(this.get("cell"),"Cell");this.set({cell:b,headerCell:a},{silent:!0})},sortValue:function(){var a=this.get("sortValue");return b.isString(a)?this[a]:b.isFunction(a)?a:function(a,b){return a.get(b)}}});b.each(["sortable","renderable","editable"],function(a){C.prototype[a]=function(){var c=this.get(a);return b.isString(c)?this[c]:b.isFunction(c)?c:!!c}});{var D=i.Columns=c.Collection.extend({model:C}),E=i.Row=c.View.extend({tagName:"tr",initialize:function(a){var b=this.columns=a.columns;b instanceof c.Collection||(b=this.columns=new D(b));for(var d=this.cells=[],e=0;e<b.length;e++)d.push(this.makeCell(b.at(e),a));this.listenTo(b,"add",function(b,c){var e=c.indexOf(b),f=this.makeCell(b,a);d.splice(e,0,f);var g=this.$el;0===e?g.prepend(f.render().$el):e===c.length-1?g.append(f.render().$el):g.children().eq(e).before(f.render().$el)}),this.listenTo(b,"remove",function(a,b,c){d[c.index].remove(),d.splice(c.index,1)})},makeCell:function(a){return new(a.get("cell"))({column:a,model:this.model})},render:function(){this.$el.empty();for(var a=document.createDocumentFragment(),b=0;b<this.cells.length;b++)a.appendChild(this.cells[b].render().el);return this.el.appendChild(a),this.delegateEvents(),this},remove:function(){for(var a=0;a<this.cells.length;a++){var b=this.cells[a];b.remove.apply(b,arguments)}return c.View.prototype.remove.apply(this,arguments)}}),F=i.EmptyRow=c.View.extend({tagName:"tr",emptyText:null,initialize:function(a){this.emptyText=a.emptyText,this.columns=a.columns},render:function(){this.$el.empty();var a=document.createElement("td");return a.setAttribute("colspan",this.columns.length),a.appendChild(document.createTextNode(b.result(this,"emptyText"))),this.el.className="empty",this.el.appendChild(a),this}}),G=i.HeaderCell=c.View.extend({tagName:"th",events:{"click a":"onClick"},initialize:function(a){this.column=a.column,this.column instanceof C||(this.column=new C(this.column));var b=this.column,c=this.collection,d=this.$el;this.listenTo(b,"change:editable change:sortable change:renderable",function(a){var b=a.changedAttributes();for(var c in b)b.hasOwnProperty(c)&&d.toggleClass(c,b[c])}),this.listenTo(b,"change:direction",this.setCellDirection),this.listenTo(b,"change:name change:label",this.render),i.callByNeed(b.editable(),b,c)&&d.addClass("editable"),i.callByNeed(b.sortable(),b,c)&&d.addClass("sortable"),i.callByNeed(b.renderable(),b,c)&&d.addClass("renderable"),this.listenTo(c.fullCollection||c,"sort",this.removeCellDirection)},removeCellDirection:function(){this.$el.removeClass("ascending").removeClass("descending"),this.column.set("direction",null)},setCellDirection:function(a,b){this.$el.removeClass("ascending").removeClass("descending"),a.cid==this.column.cid&&this.$el.addClass(b)},onClick:function(a){function b(a,b){"ascending"===d.get("direction")?e.trigger(f,b,"descending"):"descending"===d.get("direction")?e.trigger(f,b,null):e.trigger(f,b,"ascending")}function c(a,b){"ascending"===d.get("direction")?e.trigger(f,b,"descending"):e.trigger(f,b,"ascending")}a.preventDefault();var d=this.column,e=this.collection,f="backgrid:sort",g=i.callByNeed(d.sortable(),d,this.collection);if(g){var h=d.get("sortType");"toggle"===h?c(this,d):b(this,d)}},render:function(){this.$el.empty();var a,b=this.column,c=i.callByNeed(b.sortable(),b,this.collection);return a=c?h("<a>").text(b.get("label")).append("<b class='sort-caret'></b>"):document.createTextNode(b.get("label")),this.$el.append(a),this.$el.addClass(b.get("name")),this.$el.addClass(b.get("direction")),this.delegateEvents(),this}}),H=(i.HeaderRow=i.Row.extend({requiredOptions:["columns","collection"],initialize:function(){i.Row.prototype.initialize.apply(this,arguments)},makeCell:function(a,b){var c=a.get("headerCell")||b.headerCell||G;return c=new c({column:a,collection:this.collection})}}),i.Header=c.View.extend({tagName:"thead",initialize:function(a){this.columns=a.columns,this.columns instanceof c.Collection||(this.columns=new D(this.columns)),this.row=new i.HeaderRow({columns:this.columns,collection:this.collection})},render:function(){return this.$el.append(this.row.render().$el),this.delegateEvents(),this},remove:function(){return this.row.remove.apply(this.row,arguments),c.View.prototype.remove.apply(this,arguments)}})),I=i.Body=c.View.extend({tagName:"tbody",initialize:function(a){this.columns=a.columns,this.columns instanceof c.Collection||(this.columns=new D(this.columns)),this.row=a.row||E,this.rows=this.collection.map(function(a){var b=new this.row({columns:this.columns,model:a});return b},this),this.emptyText=a.emptyText,this._unshiftEmptyRowMayBe();var b=this.collection;this.listenTo(b,"add",this.insertRow),this.listenTo(b,"remove",this.removeRow),this.listenTo(b,"sort",this.refresh),this.listenTo(b,"reset",this.refresh),this.listenTo(b,"backgrid:sort",this.sort),this.listenTo(b,"backgrid:edited",this.moveToNextCell)},_unshiftEmptyRowMayBe:function(){0===this.rows.length&&null!=this.emptyText&&this.rows.unshift(new F({emptyText:this.emptyText,columns:this.columns}))},insertRow:function(a,b,d){if(this.rows[0]instanceof F&&this.rows.pop().remove(),!(b instanceof c.Collection||d))return void this.collection.add(a,d=b);var e=new this.row({columns:this.columns,model:a}),f=b.indexOf(a);this.rows.splice(f,0,e);var g=this.$el,h=g.children(),i=e.render().$el;return f>=h.length?g.append(i):h.eq(f).before(i),this},removeRow:function(a,c,d){return d?((b.isUndefined(d.render)||d.render)&&this.rows[d.index].remove(),this.rows.splice(d.index,1),this._unshiftEmptyRowMayBe(),this):(this.collection.remove(a,d=c),void this._unshiftEmptyRowMayBe())},refresh:function(){for(var a=0;a<this.rows.length;a++)this.rows[a].remove();return this.rows=this.collection.map(function(a){var b=new this.row({columns:this.columns,model:a});return b},this),this._unshiftEmptyRowMayBe(),this.render(),this.collection.trigger("backgrid:refresh",this),this},render:function(){this.$el.empty();for(var a=document.createDocumentFragment(),b=0;b<this.rows.length;b++){var c=this.rows[b];a.appendChild(c.render().el)}return this.el.appendChild(a),this.delegateEvents(),this},remove:function(){for(var a=0;a<this.rows.length;a++){var b=this.rows[a];b.remove.apply(b,arguments)}return c.View.prototype.remove.apply(this,arguments)},sort:function(a,d){if(!b.contains(["ascending","descending",null],d))throw new RangeError('direction must be one of "ascending", "descending" or `null`');b.isString(a)&&(a=this.columns.findWhere({name:a}));var e,f=this.collection;e="ascending"===d?-1:"descending"===d?1:null;var g=this.makeComparator(a.get("name"),e,e?a.sortValue():function(a){return 1*a.cid.replace("c","")});return c.PageableCollection&&f instanceof c.PageableCollection?(f.setSorting(e&&a.get("name"),e,{sortValue:a.sortValue()}),f.fullCollection?(null==f.fullCollection.comparator&&(f.fullCollection.comparator=g),f.fullCollection.sort(),f.trigger("backgrid:sorted",a,d,f)):f.fetch({reset:!0,success:function(){f.trigger("backgrid:sorted",a,d,f)}})):(f.comparator=g,f.sort(),f.trigger("backgrid:sorted",a,d,f)),a.set("direction",d),this},makeComparator:function(a,b,c){return function(d,e){var f,g=c(d,a),h=c(e,a);return 1===b&&(f=g,g=h,h=f),g===h?0:h>g?-1:1}},moveToNextCell:function(a,b,c){var d,e,f,g,h,j=this.collection.indexOf(a),k=this.columns.indexOf(b);if(this.rows[j].cells[k].exitEditMode(),c.moveUp()||c.moveDown()||c.moveLeft()||c.moveRight()||c.save()){var l=this.columns.length,m=l*this.collection.length;if(c.moveUp()||c.moveDown()){g=j+(c.moveUp()?-1:1);var n=this.rows[g];n?(d=n.cells[k],i.callByNeed(d.column.editable(),d.column,a)&&(d.enterEditMode(),a.trigger("backgrid:next",g,k,!1))):a.trigger("backgrid:next",g,k,!0)}else if(c.moveLeft()||c.moveRight()){for(var o=c.moveRight(),p=j*l+k+(o?1:-1);p>=0&&m>p;o?p++:p--)if(g=~~(p/l),h=p-g*l,d=this.rows[g].cells[h],e=i.callByNeed(d.column.renderable(),d.column,d.model),f=i.callByNeed(d.column.editable(),d.column,a),e&&f){d.enterEditMode(),a.trigger("backgrid:next",g,h,!1);break}p==m&&a.trigger("backgrid:next",~~(p/l),p-g*l,!0)}}return this}});i.Footer=c.View.extend({tagName:"tfoot",initialize:function(a){this.columns=a.columns,this.columns instanceof c.Collection||(this.columns=new i.Columns(this.columns))}}),i.Grid=c.View.extend({tagName:"table",className:"backgrid",header:H,body:I,footer:null,initialize:function(a){a.columns instanceof c.Collection||(a.columns=new D(a.columns)),this.columns=a.columns;var d=b.omit(a,["el","id","attributes","className","tagName","events"]);this.body=a.body||this.body,this.body=new this.body(d),this.header=a.header||this.header,this.header&&(this.header=new this.header(d)),this.footer=a.footer||this.footer,this.footer&&(this.footer=new this.footer(d)),this.listenTo(this.columns,"reset",function(){this.header&&(this.header=new(this.header.remove().constructor)(d)),this.body=new(this.body.remove().constructor)(d),this.footer&&(this.footer=new(this.footer.remove().constructor)(d)),this.render()})},insertRow:function(){return this.body.insertRow.apply(this.body,arguments),this},removeRow:function(){return this.body.removeRow.apply(this.body,arguments),this},insertColumn:function(){return this.columns.add.apply(this.columns,arguments),this},removeColumn:function(){return this.columns.remove.apply(this.columns,arguments),this},sort:function(){return this.body.sort.apply(this.body,arguments),this},render:function(){return this.$el.empty(),this.header&&this.$el.append(this.header.render().$el),this.footer&&this.$el.append(this.footer.render().$el),this.$el.append(this.body.render().$el),this.delegateEvents(),this.trigger("backgrid:rendered",this),this},remove:function(){return this.header&&this.header.remove.apply(this.header,arguments),this.body.remove.apply(this.body,arguments),this.footer&&this.footer.remove.apply(this.footer,arguments),c.View.prototype.remove.apply(this,arguments)}})}return i});
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/backgrid-paginator.js b/htrace-core/src/web/lib/js/backgrid-paginator-0.3.5.js
similarity index 100%
rename from htrace-core/src/web/lib/js/backgrid-paginator.js
rename to htrace-core/src/web/lib/js/backgrid-paginator-0.3.5.js
diff --git a/htrace-core/src/web/lib/js/backgrid-paginator.min.js b/htrace-core/src/web/lib/js/backgrid-paginator.min.js
deleted file mode 100644
index 5d3bc0b..0000000
--- a/htrace-core/src/web/lib/js/backgrid-paginator.min.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/*
- backgrid-paginator
- http://github.com/wyuenho/backgrid
-
- Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
- Licensed under the MIT @license.
- */
-// Trunk from Commit ID: 669bca9383f1cafa53dc0efefe96a83c5315684d.
-!function(a,b){"object"==typeof exports&&(module.exports=b(require("underscore"),require("backbone"),require("backgrid"),require("backbone.paginator"))),"function"==typeof define&&define.amd?define(["underscore","backbone","backgrid","backbone.paginator"],b):b(a._,a.Backbone,a.Backgrid)}(this,function(a,b,c){"use strict";var d=c.Extension.PageHandle=b.View.extend({tagName:"li",events:{"click a":"changePage"},title:function(a){return"Page "+a.label},isRewind:!1,isBack:!1,isForward:!1,isFastForward:!1,initialize:function(b){var c=this.collection,d=c.state,e=d.currentPage,f=d.firstPage,g=d.lastPage;a.extend(this,a.pick(b,["isRewind","isBack","isForward","isFastForward"]));var h;this.isRewind?h=f:this.isBack?h=Math.max(f,e-1):this.isForward?h=Math.min(g,e+1):this.isFastForward?h=g:(h=+b.pageIndex,h=f?h+1:h),this.pageIndex=h,this.label=(b.label||(f?h:h+1))+"";var i=b.title||this.title;this.title=a.isFunction(i)?i({label:this.label}):i},render:function(){this.$el.empty();var a=document.createElement("a");a.href="#",this.title&&(a.title=this.title),a.innerHTML=this.label,this.el.appendChild(a);var b=this.collection,c=b.state,d=c.currentPage,e=this.pageIndex;return this.isRewind&&d==c.firstPage||this.isBack&&!b.hasPreviousPage()||this.isForward&&!b.hasNextPage()||this.isFastForward&&(d==c.lastPage||c.totalPages<1)?this.$el.addClass("disabled"):this.isRewind||this.isBack||this.isForward||this.isFastForward||c.currentPage!=e||this.$el.addClass("active"),this.delegateEvents(),this},changePage:function(a){a.preventDefault();var b=this.$el,c=this.collection;return b.hasClass("active")||b.hasClass("disabled")||(this.isRewind?c.getFirstPage():this.isBack?c.getPreviousPage():this.isForward?c.getNextPage():this.isFastForward?c.getLastPage():c.getPage(this.pageIndex,{reset:!0})),this}}),e=c.Extension.Paginator=b.View.extend({className:"backgrid-paginator",windowSize:10,slideScale:.5,controls:{rewind:{label:"《",title:"First"},back:{label:"〈",title:"Previous"},forward:{label:"〉",title:"Next"},fastForward:{label:"》",title:"Last"}},renderIndexedPageHandles:!0,pageHandle:d,goBackFirstOnSort:!0,initialize:function(b){var c=this;c.controls=a.defaults(b.controls||{},c.controls,e.prototype.controls),a.extend(c,a.pick(b||{},"windowSize","pageHandle","slideScale","goBackFirstOnSort","renderIndexedPageHandles"));var d=c.collection;c.listenTo(d,"add",c.render),c.listenTo(d,"remove",c.render),c.listenTo(d,"reset",c.render),c.listenTo(d,"backgrid:sorted",function(){c.goBackFirstOnSort&&d.getFirstPage({reset:!0})})},slideMaybe:function(a,b,c,d){return Math.round(c%d/d)},slideThisMuch:function(a,b,c,d,e){return~~(d*e)},_calculateWindow:function(){var a=this.collection,b=a.state,c=b.firstPage,d=+b.lastPage;d=Math.max(0,c?d-1:d);var e=Math.max(b.currentPage,b.firstPage);e=c?e-1:e;var f=this.windowSize,g=this.slideScale,h=Math.floor(e/f)*f;e<=d-this.slideThisMuch()&&(h+=this.slideMaybe(c,d,e,f,g)*this.slideThisMuch(c,d,e,f,g));var i=Math.min(d+1,h+f);return[h,i]},makeHandles:function(){var b=[],c=this.collection,d=this._calculateWindow(),e=d[0],f=d[1];if(this.renderIndexedPageHandles)for(var g=e;f>g;g++)b.push(new this.pageHandle({collection:c,pageIndex:g}));var h=this.controls;return a.each(["back","rewind","forward","fastForward"],function(a){var d=h[a];if(d){var e={collection:c,title:d.title,label:d.label};e["is"+a.slice(0,1).toUpperCase()+a.slice(1)]=!0;var f=new this.pageHandle(e);"rewind"==a||"back"==a?b.unshift(f):b.push(f)}},this),b},render:function(){if(this.$el.empty(),this.handles)for(var a=0,b=this.handles.length;b>a;a++)this.handles[a].remove();for(var c=this.handles=this.makeHandles(),d=document.createElement("ul"),a=0;a<c.length;a++)d.appendChild(c[a].render().el);return this.el.appendChild(d),this}})});
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/underscore-1.7.0.js b/htrace-core/src/web/lib/js/underscore-1.7.0.js
new file mode 100644
index 0000000..d5b3375
--- /dev/null
+++ b/htrace-core/src/web/lib/js/underscore-1.7.0.js
@@ -0,0 +1,1416 @@
+//     Underscore.js 1.7.0
+//     http://underscorejs.org
+//     (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+//     Underscore may be freely distributed under the MIT license.
+
+(function() {
+
+  // Baseline setup
+  // --------------
+
+  // Establish the root object, `window` in the browser, or `exports` on the server.
+  var root = this;
+
+  // Save the previous value of the `_` variable.
+  var previousUnderscore = root._;
+
+  // Save bytes in the minified (but not gzipped) version:
+  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
+
+  // Create quick reference variables for speed access to core prototypes.
+  var
+    push             = ArrayProto.push,
+    slice            = ArrayProto.slice,
+    concat           = ArrayProto.concat,
+    toString         = ObjProto.toString,
+    hasOwnProperty   = ObjProto.hasOwnProperty;
+
+  // All **ECMAScript 5** native function implementations that we hope to use
+  // are declared here.
+  var
+    nativeIsArray      = Array.isArray,
+    nativeKeys         = Object.keys,
+    nativeBind         = FuncProto.bind;
+
+  // Create a safe reference to the Underscore object for use below.
+  var _ = function(obj) {
+    if (obj instanceof _) return obj;
+    if (!(this instanceof _)) return new _(obj);
+    this._wrapped = obj;
+  };
+
+  // Export the Underscore object for **Node.js**, with
+  // backwards-compatibility for the old `require()` API. If we're in
+  // the browser, add `_` as a global object.
+  if (typeof exports !== 'undefined') {
+    if (typeof module !== 'undefined' && module.exports) {
+      exports = module.exports = _;
+    }
+    exports._ = _;
+  } else {
+    root._ = _;
+  }
+
+  // Current version.
+  _.VERSION = '1.7.0';
+
+  // Internal function that returns an efficient (for current engines) version
+  // of the passed-in callback, to be repeatedly applied in other Underscore
+  // functions.
+  var createCallback = function(func, context, argCount) {
+    if (context === void 0) return func;
+    switch (argCount == null ? 3 : argCount) {
+      case 1: return function(value) {
+        return func.call(context, value);
+      };
+      case 2: return function(value, other) {
+        return func.call(context, value, other);
+      };
+      case 3: return function(value, index, collection) {
+        return func.call(context, value, index, collection);
+      };
+      case 4: return function(accumulator, value, index, collection) {
+        return func.call(context, accumulator, value, index, collection);
+      };
+    }
+    return function() {
+      return func.apply(context, arguments);
+    };
+  };
+
+  // A mostly-internal function to generate callbacks that can be applied
+  // to each element in a collection, returning the desired result — either
+  // identity, an arbitrary callback, a property matcher, or a property accessor.
+  _.iteratee = function(value, context, argCount) {
+    if (value == null) return _.identity;
+    if (_.isFunction(value)) return createCallback(value, context, argCount);
+    if (_.isObject(value)) return _.matches(value);
+    return _.property(value);
+  };
+
+  // Collection Functions
+  // --------------------
+
+  // The cornerstone, an `each` implementation, aka `forEach`.
+  // Handles raw objects in addition to array-likes. Treats all
+  // sparse array-likes as if they were dense.
+  _.each = _.forEach = function(obj, iteratee, context) {
+    if (obj == null) return obj;
+    iteratee = createCallback(iteratee, context);
+    var i, length = obj.length;
+    if (length === +length) {
+      for (i = 0; i < length; i++) {
+        iteratee(obj[i], i, obj);
+      }
+    } else {
+      var keys = _.keys(obj);
+      for (i = 0, length = keys.length; i < length; i++) {
+        iteratee(obj[keys[i]], keys[i], obj);
+      }
+    }
+    return obj;
+  };
+
+  // Return the results of applying the iteratee to each element.
+  _.map = _.collect = function(obj, iteratee, context) {
+    if (obj == null) return [];
+    iteratee = _.iteratee(iteratee, context);
+    var keys = obj.length !== +obj.length && _.keys(obj),
+        length = (keys || obj).length,
+        results = Array(length),
+        currentKey;
+    for (var index = 0; index < length; index++) {
+      currentKey = keys ? keys[index] : index;
+      results[index] = iteratee(obj[currentKey], currentKey, obj);
+    }
+    return results;
+  };
+
+  var reduceError = 'Reduce of empty array with no initial value';
+
+  // **Reduce** builds up a single result from a list of values, aka `inject`,
+  // or `foldl`.
+  _.reduce = _.foldl = _.inject = function(obj, iteratee, memo, context) {
+    if (obj == null) obj = [];
+    iteratee = createCallback(iteratee, context, 4);
+    var keys = obj.length !== +obj.length && _.keys(obj),
+        length = (keys || obj).length,
+        index = 0, currentKey;
+    if (arguments.length < 3) {
+      if (!length) throw new TypeError(reduceError);
+      memo = obj[keys ? keys[index++] : index++];
+    }
+    for (; index < length; index++) {
+      currentKey = keys ? keys[index] : index;
+      memo = iteratee(memo, obj[currentKey], currentKey, obj);
+    }
+    return memo;
+  };
+
+  // The right-associative version of reduce, also known as `foldr`.
+  _.reduceRight = _.foldr = function(obj, iteratee, memo, context) {
+    if (obj == null) obj = [];
+    iteratee = createCallback(iteratee, context, 4);
+    var keys = obj.length !== + obj.length && _.keys(obj),
+        index = (keys || obj).length,
+        currentKey;
+    if (arguments.length < 3) {
+      if (!index) throw new TypeError(reduceError);
+      memo = obj[keys ? keys[--index] : --index];
+    }
+    while (index--) {
+      currentKey = keys ? keys[index] : index;
+      memo = iteratee(memo, obj[currentKey], currentKey, obj);
+    }
+    return memo;
+  };
+
+  // Return the first value which passes a truth test. Aliased as `detect`.
+  _.find = _.detect = function(obj, predicate, context) {
+    var result;
+    predicate = _.iteratee(predicate, context);
+    _.some(obj, function(value, index, list) {
+      if (predicate(value, index, list)) {
+        result = value;
+        return true;
+      }
+    });
+    return result;
+  };
+
+  // Return all the elements that pass a truth test.
+  // Aliased as `select`.
+  _.filter = _.select = function(obj, predicate, context) {
+    var results = [];
+    if (obj == null) return results;
+    predicate = _.iteratee(predicate, context);
+    _.each(obj, function(value, index, list) {
+      if (predicate(value, index, list)) results.push(value);
+    });
+    return results;
+  };
+
+  // Return all the elements for which a truth test fails.
+  _.reject = function(obj, predicate, context) {
+    return _.filter(obj, _.negate(_.iteratee(predicate)), context);
+  };
+
+  // Determine whether all of the elements match a truth test.
+  // Aliased as `all`.
+  _.every = _.all = function(obj, predicate, context) {
+    if (obj == null) return true;
+    predicate = _.iteratee(predicate, context);
+    var keys = obj.length !== +obj.length && _.keys(obj),
+        length = (keys || obj).length,
+        index, currentKey;
+    for (index = 0; index < length; index++) {
+      currentKey = keys ? keys[index] : index;
+      if (!predicate(obj[currentKey], currentKey, obj)) return false;
+    }
+    return true;
+  };
+
+  // Determine if at least one element in the object matches a truth test.
+  // Aliased as `any`.
+  _.some = _.any = function(obj, predicate, context) {
+    if (obj == null) return false;
+    predicate = _.iteratee(predicate, context);
+    var keys = obj.length !== +obj.length && _.keys(obj),
+        length = (keys || obj).length,
+        index, currentKey;
+    for (index = 0; index < length; index++) {
+      currentKey = keys ? keys[index] : index;
+      if (predicate(obj[currentKey], currentKey, obj)) return true;
+    }
+    return false;
+  };
+
+  // Determine if the array or object contains a given value (using `===`).
+  // Aliased as `include`.
+  _.contains = _.include = function(obj, target) {
+    if (obj == null) return false;
+    if (obj.length !== +obj.length) obj = _.values(obj);
+    return _.indexOf(obj, target) >= 0;
+  };
+
+  // Invoke a method (with arguments) on every item in a collection.
+  _.invoke = function(obj, method) {
+    var args = slice.call(arguments, 2);
+    var isFunc = _.isFunction(method);
+    return _.map(obj, function(value) {
+      return (isFunc ? method : value[method]).apply(value, args);
+    });
+  };
+
+  // Convenience version of a common use case of `map`: fetching a property.
+  _.pluck = function(obj, key) {
+    return _.map(obj, _.property(key));
+  };
+
+  // Convenience version of a common use case of `filter`: selecting only objects
+  // containing specific `key:value` pairs.
+  _.where = function(obj, attrs) {
+    return _.filter(obj, _.matches(attrs));
+  };
+
+  // Convenience version of a common use case of `find`: getting the first object
+  // containing specific `key:value` pairs.
+  _.findWhere = function(obj, attrs) {
+    return _.find(obj, _.matches(attrs));
+  };
+
+  // Return the maximum element (or element-based computation).
+  _.max = function(obj, iteratee, context) {
+    var result = -Infinity, lastComputed = -Infinity,
+        value, computed;
+    if (iteratee == null && obj != null) {
+      obj = obj.length === +obj.length ? obj : _.values(obj);
+      for (var i = 0, length = obj.length; i < length; i++) {
+        value = obj[i];
+        if (value > result) {
+          result = value;
+        }
+      }
+    } else {
+      iteratee = _.iteratee(iteratee, context);
+      _.each(obj, function(value, index, list) {
+        computed = iteratee(value, index, list);
+        if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
+          result = value;
+          lastComputed = computed;
+        }
+      });
+    }
+    return result;
+  };
+
+  // Return the minimum element (or element-based computation).
+  _.min = function(obj, iteratee, context) {
+    var result = Infinity, lastComputed = Infinity,
+        value, computed;
+    if (iteratee == null && obj != null) {
+      obj = obj.length === +obj.length ? obj : _.values(obj);
+      for (var i = 0, length = obj.length; i < length; i++) {
+        value = obj[i];
+        if (value < result) {
+          result = value;
+        }
+      }
+    } else {
+      iteratee = _.iteratee(iteratee, context);
+      _.each(obj, function(value, index, list) {
+        computed = iteratee(value, index, list);
+        if (computed < lastComputed || computed === Infinity && result === Infinity) {
+          result = value;
+          lastComputed = computed;
+        }
+      });
+    }
+    return result;
+  };
+
+  // Shuffle a collection, using the modern version of the
+  // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
+  _.shuffle = function(obj) {
+    var set = obj && obj.length === +obj.length ? obj : _.values(obj);
+    var length = set.length;
+    var shuffled = Array(length);
+    for (var index = 0, rand; index < length; index++) {
+      rand = _.random(0, index);
+      if (rand !== index) shuffled[index] = shuffled[rand];
+      shuffled[rand] = set[index];
+    }
+    return shuffled;
+  };
+
+  // Sample **n** random values from a collection.
+  // If **n** is not specified, returns a single random element.
+  // The internal `guard` argument allows it to work with `map`.
+  _.sample = function(obj, n, guard) {
+    if (n == null || guard) {
+      if (obj.length !== +obj.length) obj = _.values(obj);
+      return obj[_.random(obj.length - 1)];
+    }
+    return _.shuffle(obj).slice(0, Math.max(0, n));
+  };
+
+  // Sort the object's values by a criterion produced by an iteratee.
+  _.sortBy = function(obj, iteratee, context) {
+    iteratee = _.iteratee(iteratee, context);
+    return _.pluck(_.map(obj, function(value, index, list) {
+      return {
+        value: value,
+        index: index,
+        criteria: iteratee(value, index, list)
+      };
+    }).sort(function(left, right) {
+      var a = left.criteria;
+      var b = right.criteria;
+      if (a !== b) {
+        if (a > b || a === void 0) return 1;
+        if (a < b || b === void 0) return -1;
+      }
+      return left.index - right.index;
+    }), 'value');
+  };
+
+  // An internal function used for aggregate "group by" operations.
+  var group = function(behavior) {
+    return function(obj, iteratee, context) {
+      var result = {};
+      iteratee = _.iteratee(iteratee, context);
+      _.each(obj, function(value, index) {
+        var key = iteratee(value, index, obj);
+        behavior(result, value, key);
+      });
+      return result;
+    };
+  };
+
+  // Groups the object's values by a criterion. Pass either a string attribute
+  // to group by, or a function that returns the criterion.
+  _.groupBy = group(function(result, value, key) {
+    if (_.has(result, key)) result[key].push(value); else result[key] = [value];
+  });
+
+  // Indexes the object's values by a criterion, similar to `groupBy`, but for
+  // when you know that your index values will be unique.
+  _.indexBy = group(function(result, value, key) {
+    result[key] = value;
+  });
+
+  // Counts instances of an object that group by a certain criterion. Pass
+  // either a string attribute to count by, or a function that returns the
+  // criterion.
+  _.countBy = group(function(result, value, key) {
+    if (_.has(result, key)) result[key]++; else result[key] = 1;
+  });
+
+  // Use a comparator function to figure out the smallest index at which
+  // an object should be inserted so as to maintain order. Uses binary search.
+  _.sortedIndex = function(array, obj, iteratee, context) {
+    iteratee = _.iteratee(iteratee, context, 1);
+    var value = iteratee(obj);
+    var low = 0, high = array.length;
+    while (low < high) {
+      var mid = low + high >>> 1;
+      if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
+    }
+    return low;
+  };
+
+  // Safely create a real, live array from anything iterable.
+  _.toArray = function(obj) {
+    if (!obj) return [];
+    if (_.isArray(obj)) return slice.call(obj);
+    if (obj.length === +obj.length) return _.map(obj, _.identity);
+    return _.values(obj);
+  };
+
+  // Return the number of elements in an object.
+  _.size = function(obj) {
+    if (obj == null) return 0;
+    return obj.length === +obj.length ? obj.length : _.keys(obj).length;
+  };
+
+  // Split a collection into two arrays: one whose elements all satisfy the given
+  // predicate, and one whose elements all do not satisfy the predicate.
+  _.partition = function(obj, predicate, context) {
+    predicate = _.iteratee(predicate, context);
+    var pass = [], fail = [];
+    _.each(obj, function(value, key, obj) {
+      (predicate(value, key, obj) ? pass : fail).push(value);
+    });
+    return [pass, fail];
+  };
+
+  // Array Functions
+  // ---------------
+
+  // Get the first element of an array. Passing **n** will return the first N
+  // values in the array. Aliased as `head` and `take`. The **guard** check
+  // allows it to work with `_.map`.
+  _.first = _.head = _.take = function(array, n, guard) {
+    if (array == null) return void 0;
+    if (n == null || guard) return array[0];
+    if (n < 0) return [];
+    return slice.call(array, 0, n);
+  };
+
+  // Returns everything but the last entry of the array. Especially useful on
+  // the arguments object. Passing **n** will return all the values in
+  // the array, excluding the last N. The **guard** check allows it to work with
+  // `_.map`.
+  _.initial = function(array, n, guard) {
+    return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));
+  };
+
+  // Get the last element of an array. Passing **n** will return the last N
+  // values in the array. The **guard** check allows it to work with `_.map`.
+  _.last = function(array, n, guard) {
+    if (array == null) return void 0;
+    if (n == null || guard) return array[array.length - 1];
+    return slice.call(array, Math.max(array.length - n, 0));
+  };
+
+  // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
+  // Especially useful on the arguments object. Passing an **n** will return
+  // the rest N values in the array. The **guard**
+  // check allows it to work with `_.map`.
+  _.rest = _.tail = _.drop = function(array, n, guard) {
+    return slice.call(array, n == null || guard ? 1 : n);
+  };
+
+  // Trim out all falsy values from an array.
+  _.compact = function(array) {
+    return _.filter(array, _.identity);
+  };
+
+  // Internal implementation of a recursive `flatten` function.
+  var flatten = function(input, shallow, strict, output) {
+    if (shallow && _.every(input, _.isArray)) {
+      return concat.apply(output, input);
+    }
+    for (var i = 0, length = input.length; i < length; i++) {
+      var value = input[i];
+      if (!_.isArray(value) && !_.isArguments(value)) {
+        if (!strict) output.push(value);
+      } else if (shallow) {
+        push.apply(output, value);
+      } else {
+        flatten(value, shallow, strict, output);
+      }
+    }
+    return output;
+  };
+
+  // Flatten out an array, either recursively (by default), or just one level.
+  _.flatten = function(array, shallow) {
+    return flatten(array, shallow, false, []);
+  };
+
+  // Return a version of the array that does not contain the specified value(s).
+  _.without = function(array) {
+    return _.difference(array, slice.call(arguments, 1));
+  };
+
+  // Produce a duplicate-free version of the array. If the array has already
+  // been sorted, you have the option of using a faster algorithm.
+  // Aliased as `unique`.
+  _.uniq = _.unique = function(array, isSorted, iteratee, context) {
+    if (array == null) return [];
+    if (!_.isBoolean(isSorted)) {
+      context = iteratee;
+      iteratee = isSorted;
+      isSorted = false;
+    }
+    if (iteratee != null) iteratee = _.iteratee(iteratee, context);
+    var result = [];
+    var seen = [];
+    for (var i = 0, length = array.length; i < length; i++) {
+      var value = array[i];
+      if (isSorted) {
+        if (!i || seen !== value) result.push(value);
+        seen = value;
+      } else if (iteratee) {
+        var computed = iteratee(value, i, array);
+        if (_.indexOf(seen, computed) < 0) {
+          seen.push(computed);
+          result.push(value);
+        }
+      } else if (_.indexOf(result, value) < 0) {
+        result.push(value);
+      }
+    }
+    return result;
+  };
+
+  // Produce an array that contains the union: each distinct element from all of
+  // the passed-in arrays.
+  _.union = function() {
+    return _.uniq(flatten(arguments, true, true, []));
+  };
+
+  // Produce an array that contains every item shared between all the
+  // passed-in arrays.
+  _.intersection = function(array) {
+    if (array == null) return [];
+    var result = [];
+    var argsLength = arguments.length;
+    for (var i = 0, length = array.length; i < length; i++) {
+      var item = array[i];
+      if (_.contains(result, item)) continue;
+      for (var j = 1; j < argsLength; j++) {
+        if (!_.contains(arguments[j], item)) break;
+      }
+      if (j === argsLength) result.push(item);
+    }
+    return result;
+  };
+
+  // Take the difference between one array and a number of other arrays.
+  // Only the elements present in just the first array will remain.
+  _.difference = function(array) {
+    var rest = flatten(slice.call(arguments, 1), true, true, []);
+    return _.filter(array, function(value){
+      return !_.contains(rest, value);
+    });
+  };
+
+  // Zip together multiple lists into a single array -- elements that share
+  // an index go together.
+  _.zip = function(array) {
+    if (array == null) return [];
+    var length = _.max(arguments, 'length').length;
+    var results = Array(length);
+    for (var i = 0; i < length; i++) {
+      results[i] = _.pluck(arguments, i);
+    }
+    return results;
+  };
+
+  // Converts lists into objects. Pass either a single array of `[key, value]`
+  // pairs, or two parallel arrays of the same length -- one of keys, and one of
+  // the corresponding values.
+  _.object = function(list, values) {
+    if (list == null) return {};
+    var result = {};
+    for (var i = 0, length = list.length; i < length; i++) {
+      if (values) {
+        result[list[i]] = values[i];
+      } else {
+        result[list[i][0]] = list[i][1];
+      }
+    }
+    return result;
+  };
+
+  // Return the position of the first occurrence of an item in an array,
+  // or -1 if the item is not included in the array.
+  // If the array is large and already in sort order, pass `true`
+  // for **isSorted** to use binary search.
+  _.indexOf = function(array, item, isSorted) {
+    if (array == null) return -1;
+    var i = 0, length = array.length;
+    if (isSorted) {
+      if (typeof isSorted == 'number') {
+        i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted;
+      } else {
+        i = _.sortedIndex(array, item);
+        return array[i] === item ? i : -1;
+      }
+    }
+    for (; i < length; i++) if (array[i] === item) return i;
+    return -1;
+  };
+
+  _.lastIndexOf = function(array, item, from) {
+    if (array == null) return -1;
+    var idx = array.length;
+    if (typeof from == 'number') {
+      idx = from < 0 ? idx + from + 1 : Math.min(idx, from + 1);
+    }
+    while (--idx >= 0) if (array[idx] === item) return idx;
+    return -1;
+  };
+
+  // Generate an integer Array containing an arithmetic progression. A port of
+  // the native Python `range()` function. See
+  // [the Python documentation](http://docs.python.org/library/functions.html#range).
+  _.range = function(start, stop, step) {
+    if (arguments.length <= 1) {
+      stop = start || 0;
+      start = 0;
+    }
+    step = step || 1;
+
+    var length = Math.max(Math.ceil((stop - start) / step), 0);
+    var range = Array(length);
+
+    for (var idx = 0; idx < length; idx++, start += step) {
+      range[idx] = start;
+    }
+
+    return range;
+  };
+
+  // Function (ahem) Functions
+  // ------------------
+
+  // Reusable constructor function for prototype setting.
+  var Ctor = function(){};
+
+  // Create a function bound to a given object (assigning `this`, and arguments,
+  // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
+  // available.
+  _.bind = function(func, context) {
+    var args, bound;
+    if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
+    if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
+    args = slice.call(arguments, 2);
+    bound = function() {
+      if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
+      Ctor.prototype = func.prototype;
+      var self = new Ctor;
+      Ctor.prototype = null;
+      var result = func.apply(self, args.concat(slice.call(arguments)));
+      if (_.isObject(result)) return result;
+      return self;
+    };
+    return bound;
+  };
+
+  // Partially apply a function by creating a version that has had some of its
+  // arguments pre-filled, without changing its dynamic `this` context. _ acts
+  // as a placeholder, allowing any combination of arguments to be pre-filled.
+  _.partial = function(func) {
+    var boundArgs = slice.call(arguments, 1);
+    return function() {
+      var position = 0;
+      var args = boundArgs.slice();
+      for (var i = 0, length = args.length; i < length; i++) {
+        if (args[i] === _) args[i] = arguments[position++];
+      }
+      while (position < arguments.length) args.push(arguments[position++]);
+      return func.apply(this, args);
+    };
+  };
+
+  // Bind a number of an object's methods to that object. Remaining arguments
+  // are the method names to be bound. Useful for ensuring that all callbacks
+  // defined on an object belong to it.
+  _.bindAll = function(obj) {
+    var i, length = arguments.length, key;
+    if (length <= 1) throw new Error('bindAll must be passed function names');
+    for (i = 1; i < length; i++) {
+      key = arguments[i];
+      obj[key] = _.bind(obj[key], obj);
+    }
+    return obj;
+  };
+
+  // Memoize an expensive function by storing its results.
+  _.memoize = function(func, hasher) {
+    var memoize = function(key) {
+      var cache = memoize.cache;
+      var address = hasher ? hasher.apply(this, arguments) : key;
+      if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
+      return cache[address];
+    };
+    memoize.cache = {};
+    return memoize;
+  };
+
+  // Delays a function for the given number of milliseconds, and then calls
+  // it with the arguments supplied.
+  _.delay = function(func, wait) {
+    var args = slice.call(arguments, 2);
+    return setTimeout(function(){
+      return func.apply(null, args);
+    }, wait);
+  };
+
+  // Defers a function, scheduling it to run after the current call stack has
+  // cleared.
+  _.defer = function(func) {
+    return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
+  };
+
+  // Returns a function, that, when invoked, will only be triggered at most once
+  // during a given window of time. Normally, the throttled function will run
+  // as much as it can, without ever going more than once per `wait` duration;
+  // but if you'd like to disable the execution on the leading edge, pass
+  // `{leading: false}`. To disable execution on the trailing edge, ditto.
+  _.throttle = function(func, wait, options) {
+    var context, args, result;
+    var timeout = null;
+    var previous = 0;
+    if (!options) options = {};
+    var later = function() {
+      previous = options.leading === false ? 0 : _.now();
+      timeout = null;
+      result = func.apply(context, args);
+      if (!timeout) context = args = null;
+    };
+    return function() {
+      var now = _.now();
+      if (!previous && options.leading === false) previous = now;
+      var remaining = wait - (now - previous);
+      context = this;
+      args = arguments;
+      if (remaining <= 0 || remaining > wait) {
+        clearTimeout(timeout);
+        timeout = null;
+        previous = now;
+        result = func.apply(context, args);
+        if (!timeout) context = args = null;
+      } else if (!timeout && options.trailing !== false) {
+        timeout = setTimeout(later, remaining);
+      }
+      return result;
+    };
+  };
+
+  // Returns a function, that, as long as it continues to be invoked, will not
+  // be triggered. The function will be called after it stops being called for
+  // N milliseconds. If `immediate` is passed, trigger the function on the
+  // leading edge, instead of the trailing.
+  _.debounce = function(func, wait, immediate) {
+    var timeout, args, context, timestamp, result;
+
+    var later = function() {
+      var last = _.now() - timestamp;
+
+      if (last < wait && last > 0) {
+        timeout = setTimeout(later, wait - last);
+      } else {
+        timeout = null;
+        if (!immediate) {
+          result = func.apply(context, args);
+          if (!timeout) context = args = null;
+        }
+      }
+    };
+
+    return function() {
+      context = this;
+      args = arguments;
+      timestamp = _.now();
+      var callNow = immediate && !timeout;
+      if (!timeout) timeout = setTimeout(later, wait);
+      if (callNow) {
+        result = func.apply(context, args);
+        context = args = null;
+      }
+
+      return result;
+    };
+  };
+
+  // Returns the first function passed as an argument to the second,
+  // allowing you to adjust arguments, run code before and after, and
+  // conditionally execute the original function.
+  _.wrap = function(func, wrapper) {
+    return _.partial(wrapper, func);
+  };
+
+  // Returns a negated version of the passed-in predicate.
+  _.negate = function(predicate) {
+    return function() {
+      return !predicate.apply(this, arguments);
+    };
+  };
+
+  // Returns a function that is the composition of a list of functions, each
+  // consuming the return value of the function that follows.
+  _.compose = function() {
+    var args = arguments;
+    var start = args.length - 1;
+    return function() {
+      var i = start;
+      var result = args[start].apply(this, arguments);
+      while (i--) result = args[i].call(this, result);
+      return result;
+    };
+  };
+
+  // Returns a function that will only be executed after being called N times.
+  _.after = function(times, func) {
+    return function() {
+      if (--times < 1) {
+        return func.apply(this, arguments);
+      }
+    };
+  };
+
+  // Returns a function that will only be executed before being called N times.
+  _.before = function(times, func) {
+    var memo;
+    return function() {
+      if (--times > 0) {
+        memo = func.apply(this, arguments);
+      } else {
+        func = null;
+      }
+      return memo;
+    };
+  };
+
+  // Returns a function that will be executed at most one time, no matter how
+  // often you call it. Useful for lazy initialization.
+  _.once = _.partial(_.before, 2);
+
+  // Object Functions
+  // ----------------
+
+  // Retrieve the names of an object's properties.
+  // Delegates to **ECMAScript 5**'s native `Object.keys`
+  _.keys = function(obj) {
+    if (!_.isObject(obj)) return [];
+    if (nativeKeys) return nativeKeys(obj);
+    var keys = [];
+    for (var key in obj) if (_.has(obj, key)) keys.push(key);
+    return keys;
+  };
+
+  // Retrieve the values of an object's properties.
+  _.values = function(obj) {
+    var keys = _.keys(obj);
+    var length = keys.length;
+    var values = Array(length);
+    for (var i = 0; i < length; i++) {
+      values[i] = obj[keys[i]];
+    }
+    return values;
+  };
+
+  // Convert an object into a list of `[key, value]` pairs.
+  _.pairs = function(obj) {
+    var keys = _.keys(obj);
+    var length = keys.length;
+    var pairs = Array(length);
+    for (var i = 0; i < length; i++) {
+      pairs[i] = [keys[i], obj[keys[i]]];
+    }
+    return pairs;
+  };
+
+  // Invert the keys and values of an object. The values must be serializable.
+  _.invert = function(obj) {
+    var result = {};
+    var keys = _.keys(obj);
+    for (var i = 0, length = keys.length; i < length; i++) {
+      result[obj[keys[i]]] = keys[i];
+    }
+    return result;
+  };
+
+  // Return a sorted list of the function names available on the object.
+  // Aliased as `methods`
+  _.functions = _.methods = function(obj) {
+    var names = [];
+    for (var key in obj) {
+      if (_.isFunction(obj[key])) names.push(key);
+    }
+    return names.sort();
+  };
+
+  // Extend a given object with all the properties in passed-in object(s).
+  _.extend = function(obj) {
+    if (!_.isObject(obj)) return obj;
+    var source, prop;
+    for (var i = 1, length = arguments.length; i < length; i++) {
+      source = arguments[i];
+      for (prop in source) {
+        if (hasOwnProperty.call(source, prop)) {
+            obj[prop] = source[prop];
+        }
+      }
+    }
+    return obj;
+  };
+
+  // Return a copy of the object only containing the whitelisted properties.
+  _.pick = function(obj, iteratee, context) {
+    var result = {}, key;
+    if (obj == null) return result;
+    if (_.isFunction(iteratee)) {
+      iteratee = createCallback(iteratee, context);
+      for (key in obj) {
+        var value = obj[key];
+        if (iteratee(value, key, obj)) result[key] = value;
+      }
+    } else {
+      var keys = concat.apply([], slice.call(arguments, 1));
+      obj = new Object(obj);
+      for (var i = 0, length = keys.length; i < length; i++) {
+        key = keys[i];
+        if (key in obj) result[key] = obj[key];
+      }
+    }
+    return result;
+  };
+
+   // Return a copy of the object without the blacklisted properties.
+  _.omit = function(obj, iteratee, context) {
+    if (_.isFunction(iteratee)) {
+      iteratee = _.negate(iteratee);
+    } else {
+      var keys = _.map(concat.apply([], slice.call(arguments, 1)), String);
+      iteratee = function(value, key) {
+        return !_.contains(keys, key);
+      };
+    }
+    return _.pick(obj, iteratee, context);
+  };
+
+  // Fill in a given object with default properties.
+  _.defaults = function(obj) {
+    if (!_.isObject(obj)) return obj;
+    for (var i = 1, length = arguments.length; i < length; i++) {
+      var source = arguments[i];
+      for (var prop in source) {
+        if (obj[prop] === void 0) obj[prop] = source[prop];
+      }
+    }
+    return obj;
+  };
+
+  // Create a (shallow-cloned) duplicate of an object.
+  _.clone = function(obj) {
+    if (!_.isObject(obj)) return obj;
+    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
+  };
+
+  // Invokes interceptor with the obj, and then returns obj.
+  // The primary purpose of this method is to "tap into" a method chain, in
+  // order to perform operations on intermediate results within the chain.
+  _.tap = function(obj, interceptor) {
+    interceptor(obj);
+    return obj;
+  };
+
+  // Internal recursive comparison function for `isEqual`.
+  var eq = function(a, b, aStack, bStack) {
+    // Identical objects are equal. `0 === -0`, but they aren't identical.
+    // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
+    if (a === b) return a !== 0 || 1 / a === 1 / b;
+    // A strict comparison is necessary because `null == undefined`.
+    if (a == null || b == null) return a === b;
+    // Unwrap any wrapped objects.
+    if (a instanceof _) a = a._wrapped;
+    if (b instanceof _) b = b._wrapped;
+    // Compare `[[Class]]` names.
+    var className = toString.call(a);
+    if (className !== toString.call(b)) return false;
+    switch (className) {
+      // Strings, numbers, regular expressions, dates, and booleans are compared by value.
+      case '[object RegExp]':
+      // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
+      case '[object String]':
+        // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+        // equivalent to `new String("5")`.
+        return '' + a === '' + b;
+      case '[object Number]':
+        // `NaN`s are equivalent, but non-reflexive.
+        // Object(NaN) is equivalent to NaN
+        if (+a !== +a) return +b !== +b;
+        // An `egal` comparison is performed for other numeric values.
+        return +a === 0 ? 1 / +a === 1 / b : +a === +b;
+      case '[object Date]':
+      case '[object Boolean]':
+        // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+        // millisecond representations. Note that invalid dates with millisecond representations
+        // of `NaN` are not equivalent.
+        return +a === +b;
+    }
+    if (typeof a != 'object' || typeof b != 'object') return false;
+    // Assume equality for cyclic structures. The algorithm for detecting cyclic
+    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+    var length = aStack.length;
+    while (length--) {
+      // Linear search. Performance is inversely proportional to the number of
+      // unique nested structures.
+      if (aStack[length] === a) return bStack[length] === b;
+    }
+    // Objects with different constructors are not equivalent, but `Object`s
+    // from different frames are.
+    var aCtor = a.constructor, bCtor = b.constructor;
+    if (
+      aCtor !== bCtor &&
+      // Handle Object.create(x) cases
+      'constructor' in a && 'constructor' in b &&
+      !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
+        _.isFunction(bCtor) && bCtor instanceof bCtor)
+    ) {
+      return false;
+    }
+    // Add the first object to the stack of traversed objects.
+    aStack.push(a);
+    bStack.push(b);
+    var size, result;
+    // Recursively compare objects and arrays.
+    if (className === '[object Array]') {
+      // Compare array lengths to determine if a deep comparison is necessary.
+      size = a.length;
+      result = size === b.length;
+      if (result) {
+        // Deep compare the contents, ignoring non-numeric properties.
+        while (size--) {
+          if (!(result = eq(a[size], b[size], aStack, bStack))) break;
+        }
+      }
+    } else {
+      // Deep compare objects.
+      var keys = _.keys(a), key;
+      size = keys.length;
+      // Ensure that both objects contain the same number of properties before comparing deep equality.
+      result = _.keys(b).length === size;
+      if (result) {
+        while (size--) {
+          // Deep compare each member
+          key = keys[size];
+          if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
+        }
+      }
+    }
+    // Remove the first object from the stack of traversed objects.
+    aStack.pop();
+    bStack.pop();
+    return result;
+  };
+
+  // Perform a deep comparison to check if two objects are equal.
+  _.isEqual = function(a, b) {
+    return eq(a, b, [], []);
+  };
+
+  // Is a given array, string, or object empty?
+  // An "empty" object has no enumerable own-properties.
+  _.isEmpty = function(obj) {
+    if (obj == null) return true;
+    if (_.isArray(obj) || _.isString(obj) || _.isArguments(obj)) return obj.length === 0;
+    for (var key in obj) if (_.has(obj, key)) return false;
+    return true;
+  };
+
+  // Is a given value a DOM element?
+  _.isElement = function(obj) {
+    return !!(obj && obj.nodeType === 1);
+  };
+
+  // Is a given value an array?
+  // Delegates to ECMA5's native Array.isArray
+  _.isArray = nativeIsArray || function(obj) {
+    return toString.call(obj) === '[object Array]';
+  };
+
+  // Is a given variable an object?
+  _.isObject = function(obj) {
+    var type = typeof obj;
+    return type === 'function' || type === 'object' && !!obj;
+  };
+
+  // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
+  _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
+    _['is' + name] = function(obj) {
+      return toString.call(obj) === '[object ' + name + ']';
+    };
+  });
+
+  // Define a fallback version of the method in browsers (ahem, IE), where
+  // there isn't any inspectable "Arguments" type.
+  if (!_.isArguments(arguments)) {
+    _.isArguments = function(obj) {
+      return _.has(obj, 'callee');
+    };
+  }
+
+  // Optimize `isFunction` if appropriate. Work around an IE 11 bug.
+  if (typeof /./ !== 'function') {
+    _.isFunction = function(obj) {
+      return typeof obj == 'function' || false;
+    };
+  }
+
+  // Is a given object a finite number?
+  _.isFinite = function(obj) {
+    return isFinite(obj) && !isNaN(parseFloat(obj));
+  };
+
+  // Is the given value `NaN`? (NaN is the only number which does not equal itself).
+  _.isNaN = function(obj) {
+    return _.isNumber(obj) && obj !== +obj;
+  };
+
+  // Is a given value a boolean?
+  _.isBoolean = function(obj) {
+    return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
+  };
+
+  // Is a given value equal to null?
+  _.isNull = function(obj) {
+    return obj === null;
+  };
+
+  // Is a given variable undefined?
+  _.isUndefined = function(obj) {
+    return obj === void 0;
+  };
+
+  // Shortcut function for checking if an object has a given property directly
+  // on itself (in other words, not on a prototype).
+  _.has = function(obj, key) {
+    return obj != null && hasOwnProperty.call(obj, key);
+  };
+
+  // Utility Functions
+  // -----------------
+
+  // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
+  // previous owner. Returns a reference to the Underscore object.
+  _.noConflict = function() {
+    root._ = previousUnderscore;
+    return this;
+  };
+
+  // Keep the identity function around for default iteratees.
+  _.identity = function(value) {
+    return value;
+  };
+
+  // Predicate-generating functions. Often useful outside of Underscore.
+  _.constant = function(value) {
+    return function() {
+      return value;
+    };
+  };
+
+  _.noop = function(){};
+
+  _.property = function(key) {
+    return function(obj) {
+      return obj[key];
+    };
+  };
+
+  // Returns a predicate for checking whether an object has a given set of `key:value` pairs.
+  _.matches = function(attrs) {
+    var pairs = _.pairs(attrs), length = pairs.length;
+    return function(obj) {
+      if (obj == null) return !length;
+      obj = new Object(obj);
+      for (var i = 0; i < length; i++) {
+        var pair = pairs[i], key = pair[0];
+        if (pair[1] !== obj[key] || !(key in obj)) return false;
+      }
+      return true;
+    };
+  };
+
+  // Run a function **n** times.
+  _.times = function(n, iteratee, context) {
+    var accum = Array(Math.max(0, n));
+    iteratee = createCallback(iteratee, context, 1);
+    for (var i = 0; i < n; i++) accum[i] = iteratee(i);
+    return accum;
+  };
+
+  // Return a random integer between min and max (inclusive).
+  _.random = function(min, max) {
+    if (max == null) {
+      max = min;
+      min = 0;
+    }
+    return min + Math.floor(Math.random() * (max - min + 1));
+  };
+
+  // A (possibly faster) way to get the current timestamp as an integer.
+  _.now = Date.now || function() {
+    return new Date().getTime();
+  };
+
+   // List of HTML entities for escaping.
+  var escapeMap = {
+    '&': '&amp;',
+    '<': '&lt;',
+    '>': '&gt;',
+    '"': '&quot;',
+    "'": '&#x27;',
+    '`': '&#x60;'
+  };
+  var unescapeMap = _.invert(escapeMap);
+
+  // Functions for escaping and unescaping strings to/from HTML interpolation.
+  var createEscaper = function(map) {
+    var escaper = function(match) {
+      return map[match];
+    };
+    // Regexes for identifying a key that needs to be escaped
+    var source = '(?:' + _.keys(map).join('|') + ')';
+    var testRegexp = RegExp(source);
+    var replaceRegexp = RegExp(source, 'g');
+    return function(string) {
+      string = string == null ? '' : '' + string;
+      return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
+    };
+  };
+  _.escape = createEscaper(escapeMap);
+  _.unescape = createEscaper(unescapeMap);
+
+  // If the value of the named `property` is a function then invoke it with the
+  // `object` as context; otherwise, return it.
+  _.result = function(object, property) {
+    if (object == null) return void 0;
+    var value = object[property];
+    return _.isFunction(value) ? object[property]() : value;
+  };
+
+  // Generate a unique integer id (unique within the entire client session).
+  // Useful for temporary DOM ids.
+  var idCounter = 0;
+  _.uniqueId = function(prefix) {
+    var id = ++idCounter + '';
+    return prefix ? prefix + id : id;
+  };
+
+  // By default, Underscore uses ERB-style template delimiters, change the
+  // following template settings to use alternative delimiters.
+  _.templateSettings = {
+    evaluate    : /<%([\s\S]+?)%>/g,
+    interpolate : /<%=([\s\S]+?)%>/g,
+    escape      : /<%-([\s\S]+?)%>/g
+  };
+
+  // When customizing `templateSettings`, if you don't want to define an
+  // interpolation, evaluation or escaping regex, we need one that is
+  // guaranteed not to match.
+  var noMatch = /(.)^/;
+
+  // Certain characters need to be escaped so that they can be put into a
+  // string literal.
+  var escapes = {
+    "'":      "'",
+    '\\':     '\\',
+    '\r':     'r',
+    '\n':     'n',
+    '\u2028': 'u2028',
+    '\u2029': 'u2029'
+  };
+
+  var escaper = /\\|'|\r|\n|\u2028|\u2029/g;
+
+  var escapeChar = function(match) {
+    return '\\' + escapes[match];
+  };
+
+  // JavaScript micro-templating, similar to John Resig's implementation.
+  // Underscore templating handles arbitrary delimiters, preserves whitespace,
+  // and correctly escapes quotes within interpolated code.
+  // NB: `oldSettings` only exists for backwards compatibility.
+  _.template = function(text, settings, oldSettings) {
+    if (!settings && oldSettings) settings = oldSettings;
+    settings = _.defaults({}, settings, _.templateSettings);
+
+    // Combine delimiters into one regular expression via alternation.
+    var matcher = RegExp([
+      (settings.escape || noMatch).source,
+      (settings.interpolate || noMatch).source,
+      (settings.evaluate || noMatch).source
+    ].join('|') + '|$', 'g');
+
+    // Compile the template source, escaping string literals appropriately.
+    var index = 0;
+    var source = "__p+='";
+    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
+      source += text.slice(index, offset).replace(escaper, escapeChar);
+      index = offset + match.length;
+
+      if (escape) {
+        source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
+      } else if (interpolate) {
+        source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
+      } else if (evaluate) {
+        source += "';\n" + evaluate + "\n__p+='";
+      }
+
+      // Adobe VMs need the match returned to produce the correct offest.
+      return match;
+    });
+    source += "';\n";
+
+    // If a variable is not specified, place data values in local scope.
+    if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+
+    source = "var __t,__p='',__j=Array.prototype.join," +
+      "print=function(){__p+=__j.call(arguments,'');};\n" +
+      source + 'return __p;\n';
+
+    try {
+      var render = new Function(settings.variable || 'obj', '_', source);
+    } catch (e) {
+      e.source = source;
+      throw e;
+    }
+
+    var template = function(data) {
+      return render.call(this, data, _);
+    };
+
+    // Provide the compiled source as a convenience for precompilation.
+    var argument = settings.variable || 'obj';
+    template.source = 'function(' + argument + '){\n' + source + '}';
+
+    return template;
+  };
+
+  // Add a "chain" function. Start chaining a wrapped Underscore object.
+  _.chain = function(obj) {
+    var instance = _(obj);
+    instance._chain = true;
+    return instance;
+  };
+
+  // OOP
+  // ---------------
+  // If Underscore is called as a function, it returns a wrapped object that
+  // can be used OO-style. This wrapper holds altered versions of all the
+  // underscore functions. Wrapped objects may be chained.
+
+  // Helper function to continue chaining intermediate results.
+  var result = function(obj) {
+    return this._chain ? _(obj).chain() : obj;
+  };
+
+  // Add your own custom functions to the Underscore object.
+  _.mixin = function(obj) {
+    _.each(_.functions(obj), function(name) {
+      var func = _[name] = obj[name];
+      _.prototype[name] = function() {
+        var args = [this._wrapped];
+        push.apply(args, arguments);
+        return result.call(this, func.apply(_, args));
+      };
+    });
+  };
+
+  // Add all of the Underscore functions to the wrapper object.
+  _.mixin(_);
+
+  // Add all mutator Array functions to the wrapper.
+  _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
+    var method = ArrayProto[name];
+    _.prototype[name] = function() {
+      var obj = this._wrapped;
+      method.apply(obj, arguments);
+      if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
+      return result.call(this, obj);
+    };
+  });
+
+  // Add all accessor Array functions to the wrapper.
+  _.each(['concat', 'join', 'slice'], function(name) {
+    var method = ArrayProto[name];
+    _.prototype[name] = function() {
+      return result.call(this, method.apply(this._wrapped, arguments));
+    };
+  });
+
+  // Extracts the result from a wrapped and chained object.
+  _.prototype.value = function() {
+    return this._wrapped;
+  };
+
+  // AMD registration happens at the end for compatibility with AMD loaders
+  // that may not enforce next-turn semantics on modules. Even though general
+  // practice for AMD registration is to be anonymous, underscore registers
+  // as a named module because, like jQuery, it is a base library that is
+  // popular enough to be bundled in a third party lib, but not be part of
+  // an AMD load request. Those cases could generate an error when an
+  // anonymous define() is called outside of a loader request.
+  if (typeof define === 'function' && define.amd) {
+    define('underscore', [], function() {
+      return _;
+    });
+  }
+}.call(this));
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/underscore-1.7.0.min.js b/htrace-core/src/web/lib/js/underscore-1.7.0.min.js
deleted file mode 100644
index 9c71c0e..0000000
--- a/htrace-core/src/web/lib/js/underscore-1.7.0.min.js
+++ /dev/null
@@ -1,6 +0,0 @@
-//     Underscore.js 1.7.0
-//     http://underscorejs.org
-//     (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
-//     Underscore may be freely distributed under the MIT license.
-(function(){var n=this,t=n._,r=Array.prototype,e=Object.prototype,u=Function.prototype,i=r.push,a=r.slice,o=r.concat,l=e.toString,c=e.hasOwnProperty,f=Array.isArray,s=Object.keys,p=u.bind,h=function(n){return n instanceof h?n:this instanceof h?void(this._wrapped=n):new h(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=h),exports._=h):n._=h,h.VERSION="1.7.0";var g=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}};h.iteratee=function(n,t,r){return null==n?h.identity:h.isFunction(n)?g(n,t,r):h.isObject(n)?h.matches(n):h.property(n)},h.each=h.forEach=function(n,t,r){if(null==n)return n;t=g(t,r);var e,u=n.length;if(u===+u)for(e=0;u>e;e++)t(n[e],e,n);else{var i=h.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},h.map=h.collect=function(n,t,r){if(null==n)return[];t=h.iteratee(t,r);for(var e,u=n.length!==+n.length&&h.keys(n),i=(u||n).length,a=Array(i),o=0;i>o;o++)e=u?u[o]:o,a[o]=t(n[e],e,n);return a};var v="Reduce of empty array with no initial value";h.reduce=h.foldl=h.inject=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length,o=0;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[o++]:o++]}for(;a>o;o++)u=i?i[o]:o,r=t(r,n[u],u,n);return r},h.reduceRight=h.foldr=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[--a]:--a]}for(;a--;)u=i?i[a]:a,r=t(r,n[u],u,n);return r},h.find=h.detect=function(n,t,r){var e;return t=h.iteratee(t,r),h.some(n,function(n,r,u){return t(n,r,u)?(e=n,!0):void 0}),e},h.filter=h.select=function(n,t,r){var e=[];return null==n?e:(t=h.iteratee(t,r),h.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e)},h.reject=function(n,t,r){return h.filter(n,h.negate(h.iteratee(t)),r)},h.every=h.all=function(n,t,r){if(null==n)return!0;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,!t(n[u],u,n))return!1;return!0},h.some=h.any=function(n,t,r){if(null==n)return!1;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,t(n[u],u,n))return!0;return!1},h.contains=h.include=function(n,t){return null==n?!1:(n.length!==+n.length&&(n=h.values(n)),h.indexOf(n,t)>=0)},h.invoke=function(n,t){var r=a.call(arguments,2),e=h.isFunction(t);return h.map(n,function(n){return(e?t:n[t]).apply(n,r)})},h.pluck=function(n,t){return h.map(n,h.property(t))},h.where=function(n,t){return h.filter(n,h.matches(t))},h.findWhere=function(n,t){return h.find(n,h.matches(t))},h.max=function(n,t,r){var e,u,i=-1/0,a=-1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],e>i&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(u>a||u===-1/0&&i===-1/0)&&(i=n,a=u)});return i},h.min=function(n,t,r){var e,u,i=1/0,a=1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],i>e&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(a>u||1/0===u&&1/0===i)&&(i=n,a=u)});return i},h.shuffle=function(n){for(var t,r=n&&n.length===+n.length?n:h.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=h.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},h.sample=function(n,t,r){return null==t||r?(n.length!==+n.length&&(n=h.values(n)),n[h.random(n.length-1)]):h.shuffle(n).slice(0,Math.max(0,t))},h.sortBy=function(n,t,r){return t=h.iteratee(t,r),h.pluck(h.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var m=function(n){return function(t,r,e){var u={};return r=h.iteratee(r,e),h.each(t,function(e,i){var a=r(e,i,t);n(u,e,a)}),u}};h.groupBy=m(function(n,t,r){h.has(n,r)?n[r].push(t):n[r]=[t]}),h.indexBy=m(function(n,t,r){n[r]=t}),h.countBy=m(function(n,t,r){h.has(n,r)?n[r]++:n[r]=1}),h.sortedIndex=function(n,t,r,e){r=h.iteratee(r,e,1);for(var u=r(t),i=0,a=n.length;a>i;){var o=i+a>>>1;r(n[o])<u?i=o+1:a=o}return i},h.toArray=function(n){return n?h.isArray(n)?a.call(n):n.length===+n.length?h.map(n,h.identity):h.values(n):[]},h.size=function(n){return null==n?0:n.length===+n.length?n.length:h.keys(n).length},h.partition=function(n,t,r){t=h.iteratee(t,r);var e=[],u=[];return h.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},h.first=h.head=h.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:0>t?[]:a.call(n,0,t)},h.initial=function(n,t,r){return a.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},h.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:a.call(n,Math.max(n.length-t,0))},h.rest=h.tail=h.drop=function(n,t,r){return a.call(n,null==t||r?1:t)},h.compact=function(n){return h.filter(n,h.identity)};var y=function(n,t,r,e){if(t&&h.every(n,h.isArray))return o.apply(e,n);for(var u=0,a=n.length;a>u;u++){var l=n[u];h.isArray(l)||h.isArguments(l)?t?i.apply(e,l):y(l,t,r,e):r||e.push(l)}return e};h.flatten=function(n,t){return y(n,t,!1,[])},h.without=function(n){return h.difference(n,a.call(arguments,1))},h.uniq=h.unique=function(n,t,r,e){if(null==n)return[];h.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=h.iteratee(r,e));for(var u=[],i=[],a=0,o=n.length;o>a;a++){var l=n[a];if(t)a&&i===l||u.push(l),i=l;else if(r){var c=r(l,a,n);h.indexOf(i,c)<0&&(i.push(c),u.push(l))}else h.indexOf(u,l)<0&&u.push(l)}return u},h.union=function(){return h.uniq(y(arguments,!0,!0,[]))},h.intersection=function(n){if(null==n)return[];for(var t=[],r=arguments.length,e=0,u=n.length;u>e;e++){var i=n[e];if(!h.contains(t,i)){for(var a=1;r>a&&h.contains(arguments[a],i);a++);a===r&&t.push(i)}}return t},h.difference=function(n){var t=y(a.call(arguments,1),!0,!0,[]);return h.filter(n,function(n){return!h.contains(t,n)})},h.zip=function(n){if(null==n)return[];for(var t=h.max(arguments,"length").length,r=Array(t),e=0;t>e;e++)r[e]=h.pluck(arguments,e);return r},h.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},h.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=h.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}for(;u>e;e++)if(n[e]===t)return e;return-1},h.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=n.length;for("number"==typeof r&&(e=0>r?e+r+1:Math.min(e,r+1));--e>=0;)if(n[e]===t)return e;return-1},h.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=r||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=Array(e),i=0;e>i;i++,n+=r)u[i]=n;return u};var d=function(){};h.bind=function(n,t){var r,e;if(p&&n.bind===p)return p.apply(n,a.call(arguments,1));if(!h.isFunction(n))throw new TypeError("Bind must be called on a function");return r=a.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(a.call(arguments)));d.prototype=n.prototype;var u=new d;d.prototype=null;var i=n.apply(u,r.concat(a.call(arguments)));return h.isObject(i)?i:u}},h.partial=function(n){var t=a.call(arguments,1);return function(){for(var r=0,e=t.slice(),u=0,i=e.length;i>u;u++)e[u]===h&&(e[u]=arguments[r++]);for(;r<arguments.length;)e.push(arguments[r++]);return n.apply(this,e)}},h.bindAll=function(n){var t,r,e=arguments.length;if(1>=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=h.bind(n[r],n);return n},h.memoize=function(n,t){var r=function(e){var u=r.cache,i=t?t.apply(this,arguments):e;return h.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},h.delay=function(n,t){var r=a.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},h.defer=function(n){return h.delay.apply(h,[n,1].concat(a.call(arguments,1)))},h.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var l=function(){o=r.leading===!1?0:h.now(),a=null,i=n.apply(e,u),a||(e=u=null)};return function(){var c=h.now();o||r.leading!==!1||(o=c);var f=t-(c-o);return e=this,u=arguments,0>=f||f>t?(clearTimeout(a),a=null,o=c,i=n.apply(e,u),a||(e=u=null)):a||r.trailing===!1||(a=setTimeout(l,f)),i}},h.debounce=function(n,t,r){var e,u,i,a,o,l=function(){var c=h.now()-a;t>c&&c>0?e=setTimeout(l,t-c):(e=null,r||(o=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,a=h.now();var c=r&&!e;return e||(e=setTimeout(l,t)),c&&(o=n.apply(i,u),i=u=null),o}},h.wrap=function(n,t){return h.partial(t,n)},h.negate=function(n){return function(){return!n.apply(this,arguments)}},h.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},h.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},h.before=function(n,t){var r;return function(){return--n>0?r=t.apply(this,arguments):t=null,r}},h.once=h.partial(h.before,2),h.keys=function(n){if(!h.isObject(n))return[];if(s)return s(n);var t=[];for(var r in n)h.has(n,r)&&t.push(r);return t},h.values=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},h.pairs=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},h.invert=function(n){for(var t={},r=h.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},h.functions=h.methods=function(n){var t=[];for(var r in n)h.isFunction(n[r])&&t.push(r);return t.sort()},h.extend=function(n){if(!h.isObject(n))return n;for(var t,r,e=1,u=arguments.length;u>e;e++){t=arguments[e];for(r in t)c.call(t,r)&&(n[r]=t[r])}return n},h.pick=function(n,t,r){var e,u={};if(null==n)return u;if(h.isFunction(t)){t=g(t,r);for(e in n){var i=n[e];t(i,e,n)&&(u[e]=i)}}else{var l=o.apply([],a.call(arguments,1));n=new Object(n);for(var c=0,f=l.length;f>c;c++)e=l[c],e in n&&(u[e]=n[e])}return u},h.omit=function(n,t,r){if(h.isFunction(t))t=h.negate(t);else{var e=h.map(o.apply([],a.call(arguments,1)),String);t=function(n,t){return!h.contains(e,t)}}return h.pick(n,t,r)},h.defaults=function(n){if(!h.isObject(n))return n;for(var t=1,r=arguments.length;r>t;t++){var e=arguments[t];for(var u in e)n[u]===void 0&&(n[u]=e[u])}return n},h.clone=function(n){return h.isObject(n)?h.isArray(n)?n.slice():h.extend({},n):n},h.tap=function(n,t){return t(n),n};var b=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof h&&(n=n._wrapped),t instanceof h&&(t=t._wrapped);var u=l.call(n);if(u!==l.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]===n)return e[i]===t;var a=n.constructor,o=t.constructor;if(a!==o&&"constructor"in n&&"constructor"in t&&!(h.isFunction(a)&&a instanceof a&&h.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c,f;if("[object Array]"===u){if(c=n.length,f=c===t.length)for(;c--&&(f=b(n[c],t[c],r,e)););}else{var s,p=h.keys(n);if(c=p.length,f=h.keys(t).length===c)for(;c--&&(s=p[c],f=h.has(t,s)&&b(n[s],t[s],r,e)););}return r.pop(),e.pop(),f};h.isEqual=function(n,t){return b(n,t,[],[])},h.isEmpty=function(n){if(null==n)return!0;if(h.isArray(n)||h.isString(n)||h.isArguments(n))return 0===n.length;for(var t in n)if(h.has(n,t))return!1;return!0},h.isElement=function(n){return!(!n||1!==n.nodeType)},h.isArray=f||function(n){return"[object Array]"===l.call(n)},h.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},h.each(["Arguments","Function","String","Number","Date","RegExp"],function(n){h["is"+n]=function(t){return l.call(t)==="[object "+n+"]"}}),h.isArguments(arguments)||(h.isArguments=function(n){return h.has(n,"callee")}),"function"!=typeof/./&&(h.isFunction=function(n){return"function"==typeof n||!1}),h.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},h.isNaN=function(n){return h.isNumber(n)&&n!==+n},h.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===l.call(n)},h.isNull=function(n){return null===n},h.isUndefined=function(n){return n===void 0},h.has=function(n,t){return null!=n&&c.call(n,t)},h.noConflict=function(){return n._=t,this},h.identity=function(n){return n},h.constant=function(n){return function(){return n}},h.noop=function(){},h.property=function(n){return function(t){return t[n]}},h.matches=function(n){var t=h.pairs(n),r=t.length;return function(n){if(null==n)return!r;n=new Object(n);for(var e=0;r>e;e++){var u=t[e],i=u[0];if(u[1]!==n[i]||!(i in n))return!1}return!0}},h.times=function(n,t,r){var e=Array(Math.max(0,n));t=g(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},h.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},h.now=Date.now||function(){return(new Date).getTime()};var _={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","`":"&#x60;"},w=h.invert(_),j=function(n){var t=function(t){return n[t]},r="(?:"+h.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};h.escape=j(_),h.unescape=j(w),h.result=function(n,t){if(null==n)return void 0;var r=n[t];return h.isFunction(r)?n[t]():r};var x=0;h.uniqueId=function(n){var t=++x+"";return n?n+t:t},h.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var A=/(.)^/,k={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},O=/\\|'|\r|\n|\u2028|\u2029/g,F=function(n){return"\\"+k[n]};h.template=function(n,t,r){!t&&r&&(t=r),t=h.defaults({},t,h.templateSettings);var e=RegExp([(t.escape||A).source,(t.interpolate||A).source,(t.evaluate||A).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(O,F),u=o+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":a&&(i+="';\n"+a+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=new Function(t.variable||"obj","_",i)}catch(o){throw o.source=i,o}var l=function(n){return a.call(this,n,h)},c=t.variable||"obj";return l.source="function("+c+"){\n"+i+"}",l},h.chain=function(n){var t=h(n);return t._chain=!0,t};var E=function(n){return this._chain?h(n).chain():n};h.mixin=function(n){h.each(h.functions(n),function(t){var r=h[t]=n[t];h.prototype[t]=function(){var n=[this._wrapped];return i.apply(n,arguments),E.call(this,r.apply(h,n))}})},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=r[n];h.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],E.call(this,r)}}),h.each(["concat","join","slice"],function(n){var t=r[n];h.prototype[n]=function(){return E.call(this,t.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}).call(this);
-//# sourceMappingURL=underscore-min.map