Tested XOSModel methods, getChoices is missing test
diff --git a/xos/core/xoslib/.eslintrc b/xos/core/xoslib/.eslintrc
index 35b0e35..211611c 100644
--- a/xos/core/xoslib/.eslintrc
+++ b/xos/core/xoslib/.eslintrc
@@ -13,7 +13,8 @@
         "beforeColon": false,
         "afterColon": true
     }],
-    "indent": [2, 2]
+    "indent": [2, 4],
+    "valid-jsdoc": 2
   },
   "globals": {
   }
diff --git a/xos/core/xoslib/karma.conf.js b/xos/core/xoslib/karma.conf.js
index 4beb3b7..5eba2fe 100644
--- a/xos/core/xoslib/karma.conf.js
+++ b/xos/core/xoslib/karma.conf.js
@@ -1,6 +1,8 @@
 // Karma configuration
 // Generated on Tue Oct 06 2015 09:27:10 GMT+0000 (UTC)
 
+/* eslint indent: [2,2], quotes: [2, "single"]*/
+
 /*eslint-disable*/
 module.exports = function(config) {
 /*eslint-enable*/
diff --git a/xos/core/xoslib/spec/xoslib/xos-backbone.test.js b/xos/core/xoslib/spec/xoslib/xos-backbone.test.js
index ec1b939..32f1551 100644
--- a/xos/core/xoslib/spec/xoslib/xos-backbone.test.js
+++ b/xos/core/xoslib/spec/xoslib/xos-backbone.test.js
@@ -23,15 +23,75 @@
   });
 
   describe('listMethods method', () => {
-    it('should list all methods in instance', () => {
-      const instance = {
-        m1: () => {},
-        m2: () => {}
-      };
+    
+    const instance = {
+      m1: () => {},
+      m2: () => {}
+    };
 
+    it('should list all methods in instance', () => {
       let res = model.listMethods.apply(instance);
       expect(res.length).toBe(2);
       expect(res[0]).toEqual('m1');
     });
   });
+
+  describe('the Save method', () => {
+    const ctxPS = {
+      preSave: () => {}
+    };
+
+    const args = ['attr', 'opts'];
+
+    beforeEach(() => {
+      spyOn(ctxPS, 'preSave');
+      spyOn(Backbone.Model.prototype, 'save');
+    });
+
+    it('should call the preSave method', () => {
+      model.save.apply(ctxPS, args);
+      expect(ctxPS.preSave).toHaveBeenCalled();
+      expect(Backbone.Model.prototype.save).toHaveBeenCalledWith(args[0], args[1]);
+    });
+
+    it('should not call the preSave method', () => {
+      model.save.apply({}, args);
+      expect(ctxPS.preSave).not.toHaveBeenCalled();
+      expect(Backbone.Model.prototype.save).toHaveBeenCalledWith(args[0], args[1]);
+    });
+  });
+
+  describe('the getChoices method', () => {
+
+    const instance = {
+      m2mFields: {'flavors': 'flavors', 'sites': 'sites', 'images': 'images'}
+    };
+
+    xit('should be tested, what is this doing?', () => {
+      model.getChoices.apply(instance);
+    });
+  });
+
+  describe('the xosValidate method', () => {
+
+    const instance = {
+      validators: {'network_ports': ['portspec']}
+    }
+
+    const validAttrs = {network_ports: 'tcp 123'};
+
+    it('should call specified validator on a field and pass', () => {
+      let err = model.xosValidate.apply(instance, [validAttrs]);
+      expect(err).toBeUndefined();
+    });
+
+    // set wrong value and recall xosValidate
+    const invalidAttrs = {network_ports: 'abd 456'};
+
+    it('should call specified validator on a field and not pass', () => {
+      let err = model.xosValidate.apply(instance, [invalidAttrs]);
+      expect(err).not.toBeUndefined();
+      expect(err).toEqual({network_ports: 'must be a valid portspec (example: \'tcp 123, udp 456-789\')'});
+    });
+  });
 });
\ No newline at end of file
diff --git a/xos/core/xoslib/static/js/xoslib/xos-backbone.js b/xos/core/xoslib/static/js/xoslib/xos-backbone.js
index 74f62ef..3e43279 100644
--- a/xos/core/xoslib/static/js/xoslib/xos-backbone.js
+++ b/xos/core/xoslib/static/js/xoslib/xos-backbone.js
@@ -53,114 +53,123 @@
 
         /* from backbone-tastypie.js */
         url: function() {
-                    // TODO handle error if no property
-                    var url = this.attributes.resource_uri;
+            // TODO handle error if no property
+            var url = this.attributes.resource_uri;
 
-                    if (!url) {
-                        if (this.id) {
-                            url = this.urlRoot + this.id;
-                        } else {
-                            // this happens when creating a new model.
-                            url = this.urlRoot;
+            if (!url) {
+                if (this.id) {
+                    url = this.urlRoot + this.id;
+                } else {
+                    // this happens when creating a new model.
+                    url = this.urlRoot;
+                }
+            }
+
+            if (!url) {
+                // XXX I'm not sure this does anything useful
+                url = ( _.isFunction( this.collection.url ) ? this.collection.url() : this.collection.url );
+                url = url || this.urlRoot;
+            }
+
+            // remove any existing query parameters
+            url && ( url.indexOf("?") > -1 ) && ( url = url.split("?")[0] );
+
+            url && ( url += ( url.length > 0 && url.charAt( url.length - 1 ) === '/' ) ? '' : '/' );
+
+            url && ( url += "?no_hyperlinks=1" );
+
+            return url;
+        },
+
+        listMethods: function() {
+            var res = [];
+            for(var m in this) {
+                if(typeof this[m] == "function") {
+                    res.push(m)
+                }
+            }
+            return res;
+        },
+
+        save: function(attributes, options) {
+            if (this.preSave) {
+                this.preSave();
+            }
+            return Backbone.Model.prototype.save.call(this, attributes, options);
+        },
+
+        getChoices: function(fieldName, excludeChosen) {
+            choices=[];
+            console.log(xos);
+            if (fieldName in this.m2mFields) {
+                for (index in xos[this.m2mFields[fieldName]].models) {
+                    candidate = xos[this.m2mFields[fieldName]].models[index];
+                    if (excludeChosen && idInArray(candidate.id, this.attributes[fieldName])) {
+                        continue;
+                    }
+                    choices.push(candidate.id);
+                }
+            }
+            return choices;
+        },
+
+        /* If a 'validate' method is supplied, then it will be called
+           automatically on save. Unfortunately, save calls neither the
+           'error' nor the 'success' callback if the validator fails.
+
+           For now, we're calling our validator 'xosValidate' so this
+           autoamtic validation doesn't occur.
+        */
+
+        /**
+        * This should call custom validators defined in the model
+        *
+        * @param {object} attrs an object containin the model attributes
+        * @param {object} options (unused)
+        * @returns {Array} Errors list
+        */
+        xosValidate: function(attrs, options) {
+            errors = {};
+            foundErrors = false;
+            _.each(this.validators, function(validatorList, fieldName) {
+                _.each(validatorList, function(validator) {
+                    if (fieldName in attrs) {
+                        // call validateField method in xos-utils.js
+                        validatorResult = validateField(validator, attrs[fieldName], this)
+                        if (validatorResult != true) {
+                            errors[fieldName] = validatorResult;
+                            foundErrors = true;
                         }
                     }
-
-                    if (!url) {
-                        // XXX I'm not sure this does anything useful
-                        url = ( _.isFunction( this.collection.url ) ? this.collection.url() : this.collection.url );
-                        url = url || this.urlRoot;
-                    }
-
-                    // remove any existing query parameters
-                    url && ( url.indexOf("?") > -1 ) && ( url = url.split("?")[0] );
-
-                    url && ( url += ( url.length > 0 && url.charAt( url.length - 1 ) === '/' ) ? '' : '/' );
-
-                    url && ( url += "?no_hyperlinks=1" );
-
-                    return url;
-            },
-
-            listMethods: function() {
-                var res = [];
-                for(var m in this) {
-                    if(typeof this[m] == "function") {
-                        res.push(m)
-                    }
-                }
-                return res;
-            },
-
-            save: function(attributes, options) {
-                if (this.preSave) {
-                    this.preSave();
-                }
-                return Backbone.Model.prototype.save.call(this, attributes, options);
-            },
-
-            getChoices: function(fieldName, excludeChosen) {
-                choices=[];
-                if (fieldName in this.m2mFields) {
-                    for (index in xos[this.m2mFields[fieldName]].models) {
-                        candidate = xos[this.m2mFields[fieldName]].models[index];
-                        if (excludeChosen && idInArray(candidate.id, this.attributes[fieldName])) {
-                            continue;
-                        }
-                        choices.push(candidate.id);
-                    }
-                }
-                return choices;
-            },
-
-            /* If a 'validate' method is supplied, then it will be called
-               automatically on save. Unfortunately, save calls neither the
-               'error' nor the 'success' callback if the validator fails.
-
-               For now, we're calling our validator 'xosValidate' so this
-               autoamtic validation doesn't occur.
-            */
-
-            xosValidate: function(attrs, options) {
-                errors = {};
-                foundErrors = false;
-                _.each(this.validators, function(validatorList, fieldName) {
-                    _.each(validatorList, function(validator) {
-                        if (fieldName in attrs) {
-                            validatorResult = validateField(validator, attrs[fieldName], this)
-                            if (validatorResult != true) {
-                                errors[fieldName] = validatorResult;
-                                foundErrors = true;
-                            }
-                        }
-                    });
                 });
-                if (foundErrors) {
-                    return errors;
-                }
-                // backbone.js semantics -- on successful validate, return nothing
-            },
+            });
+            if (foundErrors) {
+                return errors;
+            }
+            // backbone.js semantics -- on successful validate, return nothing
+        },
 
-            /* uncommenting this would make validate() call xosValidate()
-            validate: function(attrs, options) {
-                r = this.xosValidate(attrs, options);
-                console.log("validate");
-                console.log(r);
-                return r;
-            }, */
+        /* uncommenting this would make validate() call xosValidate()
+        validate: function(attrs, options) {
+            r = this.xosValidate(attrs, options);
+            console.log("validate");
+            console.log(r);
+            return r;
+        }, */
     });
 
     XOSCollection = Backbone.Collection.extend({
         objects: function() {
-                    return this.models.map(function(element) { return element.attributes; });
-                 },
+            return this.models.map(function(element) { return element.attributes; });
+        },
 
         initialize: function(){
-          this.isLoaded = false;
-          this.failedLoad = false;
-          this.startedLoad = false;
-          this.sortVar = 'name';
-          this.sortOrder = 'asc';
-          this.on( "sort", this.sorted );
+            this.isLoaded = false;
+            this.failedLoad = false;
+            this.startedLoad = false;
+            this.sortVar = 'name';
+            this.sortOrder = 'asc';
+            this.on( "sort", this.sorted );
         },
 
         relatedCollections: [],
@@ -175,12 +184,12 @@
         },
 
         simpleComparator: function( model ){
-          parts=this.sortVar.split(".");
-          result = model.get(parts[0]);
-          for (index=1; index<parts.length; ++index) {
-              result=result[parts[index]];
-          }
-          return result;
+            parts=this.sortVar.split(".");
+            result = model.get(parts[0]);
+            for (index=1; index<parts.length; ++index) {
+                result=result[parts[index]];
+            }
+            return result;
         },
 
         comparator: function (left, right) {
@@ -190,7 +199,7 @@
             if (l === void 0) return -1;
             if (r === void 0) return 1;
 
-            if (this.sortOrder=="desc") {
+            if (this.sortOrder == "desc") {
                 return l < r ? 1 : l > r ? -1 : 0;
             } else {
                 return l < r ? -1 : l > r ? 1 : 0;
@@ -276,8 +285,8 @@
             }
 
                 // when the original success function completes mark this collection as fetched
-            var self = this,
-            successWrapper = function(success){
+            var self = this;
+            var successWrapper = function(success){
                 return function(){
                     self._fetched = true;
                     success && success.apply(this, arguments);
@@ -313,35 +322,35 @@
         */
 
         filterBy: function(fieldName, value) {
-             filtered = this.filter(function(obj) {
-                 return obj.get(fieldName) == value;
-                 });
-             return new this.constructor(filtered);
+            filtered = this.filter(function(obj) {
+                return obj.get(fieldName) == value;
+                });
+            return new this.constructor(filtered);
         },
 
         /* from backbone-tastypie.js */
         url: function( models ) {
-                    var url = this.urlRoot || ( models && models.length && models[0].urlRoot );
-                    url && ( url += ( url.length > 0 && url.charAt( url.length - 1 ) === '/' ) ? '' : '/' );
+            var url = this.urlRoot || ( models && models.length && models[0].urlRoot );
+            url && ( url += ( url.length > 0 && url.charAt( url.length - 1 ) === '/' ) ? '' : '/' );
 
-                    url && ( url += "?no_hyperlinks=1" );
+            url && ( url += "?no_hyperlinks=1" );
 
-                    if (this.currentUserCanSee) {
-                        url && ( url += "&current_user_can_see=1" );
-                    }
+            if (this.currentUserCanSee) {
+                url && ( url += "&current_user_can_see=1" );
+            }
 
-                    return url;
-            },
+            return url;
+        },
 
         listMethods: function() {
-                var res = [];
-                for(var m in this) {
-                    if(typeof this[m] == "function") {
-                        res.push(m)
-                    }
+            var res = [];
+            for(var m in this) {
+                if(typeof this[m] == "function") {
+                    res.push(m)
                 }
-                return res;
-            },
+            }
+            return res;
+        },
     });
 
     function get_defaults(modelName) {
@@ -486,27 +495,27 @@
                             });
 
         define_model(this, {urlRoot: SLICE_API,
-                           relatedCollections: {"instances": "slice", "slicePrivileges": "slice", "networks": "owner", "controller_slices": "slice"},
-                           foreignCollections: ["services", "sites"],
-                           foreignFields: {"service": "services", "site": "sites"},
-                           listFields: ["backend_status", "id", "name", "enabled", "description", "slice_url", "site", "max_instances", "service"],
-                           detailFields: ["backend_status", "backend_register", "name", "site", "enabled", "description", "slice_url", "max_instances"],
-                           inputType: {"enabled": "checkbox"},
-                           modelName: "slice",
-                           xosValidate: function(attrs, options) {
-                               errors = XOSModel.prototype.xosValidate.call(this, attrs, options);
-                               // validate that slice.name starts with site.login_base
-                               site = attrs.site || this.site;
-                               if ((site!=undefined) && (attrs.name!=undefined)) {
-                                   site = xos.sites.get(site);
-                                   if (attrs.name.indexOf(site.attributes.login_base+"_") != 0) {
+                            relatedCollections: {"instances": "slice", "slicePrivileges": "slice", "networks": "owner", "controller_slices": "slice"},
+                            foreignCollections: ["services", "sites"],
+                            foreignFields: {"service": "services", "site": "sites"},
+                            listFields: ["backend_status", "id", "name", "enabled", "description", "slice_url", "site", "max_instances", "service"],
+                            detailFields: ["backend_status", "backend_register", "name", "site", "enabled", "description", "slice_url", "max_instances"],
+                            inputType: {"enabled": "checkbox"},
+                            modelName: "slice",
+                            xosValidate: function(attrs, options) {
+                                errors = XOSModel.prototype.xosValidate.call(this, attrs, options);
+                                // validate that slice.name starts with site.login_base
+                                site = attrs.site || this.site;
+                                if ((site!=undefined) && (attrs.name!=undefined)) {
+                                    site = xos.sites.get(site);
+                                    if (attrs.name.indexOf(site.attributes.login_base+"_") != 0) {
                                         errors = errors || {};
                                         errors["name"] = "must start with " + site.attributes.login_base + "_";
-                                   }
-                               }
-                               return errors;
-                             },
-                           });
+                                    }
+                                }
+                                return errors;
+                            },
+                        });
 
         define_model(this, {urlRoot: SLICEPRIVILEGE_API,
                             foreignCollections: ["slices", "users", "sliceRoles"],
@@ -699,30 +708,30 @@
         // enhanced REST
         // XXX this really needs to somehow be combined with Slice, to avoid duplication
         define_model(this, {urlRoot: SLICEPLUS_API,
-                           relatedCollections: {"instances": "slice", "slicePrivileges": "slice", "networks": "owner"},
-                           foreignCollections: ["services", "sites"],
-                           foreignFields: {"service": "services", "site": "sites"},
-                           listFields: ["backend_status", "id", "name", "enabled", "description", "slice_url", "site", "max_instances", "service"],
-                           detailFields: ["backend_status", "backend_register", "name", "site", "enabled", "description", "slice_url", "max_instances"],
-                           inputType: {"enabled": "checkbox"},
-                           modelName: "slicePlus",
-                           collectionName: "slicesPlus",
-                           defaults: extend_defaults("slice", {"network_ports": "", "site_allocation": []}),
-                           validators: {"network_ports": ["portspec"]},
-                           xosValidate: function(attrs, options) {
-                               errors = XOSModel.prototype.xosValidate.call(this, attrs, options);
-                               // validate that slice.name starts with site.login_base
-                               site = attrs.site || this.site;
-                               if ((site!=undefined) && (attrs.name!=undefined)) {
-                                   site = xos.sites.get(site);
-                                   if (attrs.name.indexOf(site.attributes.login_base+"_") != 0) {
+                            relatedCollections: {"instances": "slice", "slicePrivileges": "slice", "networks": "owner"},
+                            foreignCollections: ["services", "sites"],
+                            foreignFields: {"service": "services", "site": "sites"},
+                            listFields: ["backend_status", "id", "name", "enabled", "description", "slice_url", "site", "max_instances", "service"],
+                            detailFields: ["backend_status", "backend_register", "name", "site", "enabled", "description", "slice_url", "max_instances"],
+                            inputType: {"enabled": "checkbox"},
+                            modelName: "slicePlus",
+                            collectionName: "slicesPlus",
+                            defaults: extend_defaults("slice", {"network_ports": "", "site_allocation": []}),
+                            validators: {"network_ports": ["portspec"]},
+                            xosValidate: function(attrs, options) {
+                                errors = XOSModel.prototype.xosValidate.call(this, attrs, options);
+                                // validate that slice.name starts with site.login_base
+                                site = attrs.site || this.site;
+                                if ((site!=undefined) && (attrs.name!=undefined)) {
+                                    site = xos.sites.get(site);
+                                    if (attrs.name.indexOf(site.attributes.login_base+"_") != 0) {
                                         errors = errors || {};
                                         errors["name"] = "must start with " + site.attributes.login_base + "_";
-                                   }
-                               }
-                               return errors;
-                             },
-                           });
+                                    }
+                                }
+                                return errors;
+                            },
+                        });
 
         define_model(this, {urlRoot: TENANTVIEW_API,
                             modelName: "tenantview",
@@ -804,13 +813,13 @@
     }
 
     (function() {
-      var _sync = Backbone.sync;
-      Backbone.sync = function(method, model, options){
-        options.beforeSend = function(xhr){
-          var token = getCookie("csrftoken");
-          xhr.setRequestHeader('X-CSRFToken', token);
+        var _sync = Backbone.sync;
+        Backbone.sync = function(method, model, options){
+            options.beforeSend = function(xhr){
+                var token = getCookie("csrftoken");
+                xhr.setRequestHeader('X-CSRFToken', token);
+            };
+            return _sync(method, model, options);
         };
-        return _sync(method, model, options);
-      };
     })();
 }
diff --git a/xos/core/xoslib/static/js/xoslib/xosHelper.js b/xos/core/xoslib/static/js/xoslib/xosHelper.js
index 1352c69..54ecba5 100644
--- a/xos/core/xoslib/static/js/xoslib/xosHelper.js
+++ b/xos/core/xoslib/static/js/xoslib/xosHelper.js
@@ -55,16 +55,16 @@
 });
 
 XOSRouter = Marionette.AppRouter.extend({
-        initialize: function() {

-            this.routeStack=[];

-        },

-

-        onRoute: function(x,y,z) {

-             this.routeStack.push(Backbone.history.fragment);

-             this.routeStack = this.routeStack.slice(-32);   // limit the size of routeStack to something reasonable

-        },

-

-        prevPage: function() {

+        initialize: function() {
+            this.routeStack=[];
+        },
+
+        onRoute: function(x,y,z) {
+             this.routeStack.push(Backbone.history.fragment);
+             this.routeStack = this.routeStack.slice(-32);   // limit the size of routeStack to something reasonable
+        },
+
+        prevPage: function() {
              return this.routeStack.slice(-1)[0];
         },
 
@@ -83,14 +83,14 @@
             }
             Marionette.AppRouter.prototype.navigate.call(this, href, options);
         },
-    });

-

-// XXX - We import backbone multiple times (BAD!) since the import happens

-//   inside of the view's html. The second time it's imported (developer

-//   view), it wipes out Backbone.Syphon. So, save it as Backbone_Syphon for

-//   now.

-Backbone_Syphon = Backbone.Syphon

-Backbone_Syphon.InputReaders.register('select', function(el) {

+    });
+
+// XXX - We import backbone multiple times (BAD!) since the import happens
+//   inside of the view's html. The second time it's imported (developer
+//   view), it wipes out Backbone.Syphon. So, save it as Backbone_Syphon for
+//   now.
+Backbone_Syphon = Backbone.Syphon
+Backbone_Syphon.InputReaders.register('select', function(el) {
     // Modify syphon so that if a select has "syphonall" in the class, then
     // the value of every option will be returned, regardless of whether of
     // not it is selected.
@@ -564,7 +564,7 @@
                 var that = this;
                 var isNew = !this.model.id;
 
-                console.log(data);
+                console.log('data', data);
 
                 this.$el.find(".help-inline").remove();
 
@@ -982,77 +982,77 @@
                 };
 
                 //console.log(aoData);
-

-                // function used to populate the DataTable with the current

-                // content of the collection

-                var populateTable = function()

-                {

-                  //console.log("populatetable!");

-

-                  // clear out old row views

-                  rows = [];

-

-                  sSearch = null;

-                  iDisplayStart = 0;

-                  iDisplayLength = 1000;

-                  sortDirs = [];

-                  sortCols = [];

-                  _.each(aoData, function(param) {

-                      if (param.name == "sSortDir_0") {

-                          sortDirs = [param.value];

-                      } else if (param.name == "iSortCol_0") {

-                          sortCols = [view.columnsByIndex[param.value].mData];

-                      } else if (param.name == "iDisplayStart") {

-                          iDisplayStart = param.value;

-                      } else if (param.name == "iDisplayLength") {

-                          iDisplayLength = param.value;

-                      } else if (param.name == "sSearch") {

-                          sSearch = param.value;

-                      }

-                  });

-

-                  aaData = view.collection.toJSON();

-

-                  // apply backbone filtering on the models

-                  if (view.filter) {

-                      aaData = aaData.filter( function(row) { model = {}; model.attributes = row; return view.filter(model); } );

-                  }

-

-                  var totalSize = aaData.length;

-

-                  // turn the ForeignKey fields into human readable things

-                  for (rowIndex in aaData) {

-                      row = aaData[rowIndex];

-                      for (fieldName in row) {

-                          if (fieldName in view.columnsByFieldName) {

-                              mSearchText = view.columnsByFieldName[fieldName].mSearchText;

-                              if (mSearchText) {

-                                  row[fieldName] = mSearchText(row[fieldName]);

-                              }

-                          }

-                      }

-                  }

-

-                  // apply datatables search

-                  if (sSearch) {

-                      aaData = aaData.filter( function(row) { return searchMatch(row, sSearch); });

-                  }

-

-                  var filteredSize = aaData.length;

-

-                  // apply datatables sort

-                  aaData.sort(function(a,b) { return compareColumns(sortCols, sortDirs, a, b); });

-

-                  // slice it for pagination

-                  if (iDisplayLength >= 0) {

-                      aaData = aaData.slice(iDisplayStart, iDisplayStart+iDisplayLength);

-                  }

-

-                  return fnCallback({iTotalRecords: totalSize,

-                         iTotalDisplayRecords: filteredSize,

-                         aaData: aaData});

-                };

-

+
+                // function used to populate the DataTable with the current
+                // content of the collection
+                var populateTable = function()
+                {
+                  //console.log("populatetable!");
+
+                  // clear out old row views
+                  rows = [];
+
+                  sSearch = null;
+                  iDisplayStart = 0;
+                  iDisplayLength = 1000;
+                  sortDirs = [];
+                  sortCols = [];
+                  _.each(aoData, function(param) {
+                      if (param.name == "sSortDir_0") {
+                          sortDirs = [param.value];
+                      } else if (param.name == "iSortCol_0") {
+                          sortCols = [view.columnsByIndex[param.value].mData];
+                      } else if (param.name == "iDisplayStart") {
+                          iDisplayStart = param.value;
+                      } else if (param.name == "iDisplayLength") {
+                          iDisplayLength = param.value;
+                      } else if (param.name == "sSearch") {
+                          sSearch = param.value;
+                      }
+                  });
+
+                  aaData = view.collection.toJSON();
+
+                  // apply backbone filtering on the models
+                  if (view.filter) {
+                      aaData = aaData.filter( function(row) { model = {}; model.attributes = row; return view.filter(model); } );
+                  }
+
+                  var totalSize = aaData.length;
+
+                  // turn the ForeignKey fields into human readable things
+                  for (rowIndex in aaData) {
+                      row = aaData[rowIndex];
+                      for (fieldName in row) {
+                          if (fieldName in view.columnsByFieldName) {
+                              mSearchText = view.columnsByFieldName[fieldName].mSearchText;
+                              if (mSearchText) {
+                                  row[fieldName] = mSearchText(row[fieldName]);
+                              }
+                          }
+                      }
+                  }
+
+                  // apply datatables search
+                  if (sSearch) {
+                      aaData = aaData.filter( function(row) { return searchMatch(row, sSearch); });
+                  }
+
+                  var filteredSize = aaData.length;
+
+                  // apply datatables sort
+                  aaData.sort(function(a,b) { return compareColumns(sortCols, sortDirs, a, b); });
+
+                  // slice it for pagination
+                  if (iDisplayLength >= 0) {
+                      aaData = aaData.slice(iDisplayStart, iDisplayStart+iDisplayLength);
+                  }
+
+                  return fnCallback({iTotalRecords: totalSize,
+                         iTotalDisplayRecords: filteredSize,
+                         aaData: aaData});
+                };
+
                 aoData.shift(); // ignore sEcho
                 populateTable();