Merge branch 'feature/tenant_view' into develop
diff --git a/.gitignore b/.gitignore
index 5408220..e91dffb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,5 @@
 *.moved-aside
 xos/configurations/frontend/Dockerfile
 xos/core/xoslib/karma-*
+xos/core/xoslib/docs
 node_modules
diff --git a/xos/configurations/common/admin-openrc.sh b/xos/configurations/common/admin-openrc.sh
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/configurations/common/admin-openrc.sh
diff --git a/xos/core/xoslib/jsdoc.conf.json b/xos/core/xoslib/jsdoc.conf.json
new file mode 100644
index 0000000..3352b79
--- /dev/null
+++ b/xos/core/xoslib/jsdoc.conf.json
@@ -0,0 +1,30 @@
+{
+  "tags": {
+    "allowUnknownTags": true
+  },
+  "plugins": ["plugins/markdown"],
+  "templates": {
+    "cleverLinks": false,
+    "monospaceLinks": false,
+    "dateFormat": "ddd MMM Do YYYY",
+    "outputSourceFiles": true,
+    "outputSourcePath": true,
+    "systemName": "XOS Lib",
+    "footer": "",
+    "copyright": "DocStrap Copyright © 2012-2014 The contributors to the JSDoc3 and DocStrap projects.",
+    "navType": "vertical",
+    "theme": "cosmo",
+    "linenums": true,
+    "collapseSymbols": false,
+    "inverseNav": true,
+    "highlightTutorialCode": true,
+    "protocol": "html://"
+  },
+  "markdown": {
+    "parser": "gfm",
+    "hardwrap": true
+  },
+  "opts": {
+    "template": "./node_modules/ink-docstrap/template/"
+  }
+}
diff --git a/xos/core/xoslib/package.json b/xos/core/xoslib/package.json
index cc23dbf..d2ed905 100644
--- a/xos/core/xoslib/package.json
+++ b/xos/core/xoslib/package.json
@@ -6,14 +6,17 @@
   "scripts": {
     "pretest": "npm install",
     "test": "karma start",
-    "lint": "eslint ."
+    "lint": "eslint .",
+    "docs": "jsdoc static/js -r -c ./jsdoc.conf.json -d docs"
   },
   "author": "",
   "license": "BSD-2-Clause",
   "devDependencies": {
     "eslint": "~1.6.0",
     "eslint-config-defaults": "^7.0.1",
+    "ink-docstrap": "^0.5.2",
     "jasmine-core": "~2.3.4",
+    "jsdoc": "^3.3.3",
     "karma": "~0.13.10",
     "karma-babel-preprocessor": "~5.2.2",
     "karma-jasmine": "~0.3.6",
diff --git a/xos/core/xoslib/spec/xoslib/xos-backbone.test.js b/xos/core/xoslib/spec/xoslib/xos-backbone.test.js
index f14275d..f97cc07 100644
--- a/xos/core/xoslib/spec/xoslib/xos-backbone.test.js
+++ b/xos/core/xoslib/spec/xoslib/xos-backbone.test.js
@@ -3,9 +3,9 @@
 describe('The Xos Backbone', () => {
 
   beforeEach(() => {
-    xosdefaults = {
+    $.extend(xosdefaults,{
       test: {config: true}
-    };
+    });
   });
 
   describe('get_defaults mehod', () => {
@@ -36,6 +36,193 @@
 
   });
 
+  describe('The define_model method', () => {
+
+    var testLib;
+
+    beforeEach(() => {
+      var TestLibDefinition = function() {
+        /* eslint-disable no-invalid-this*/
+        this.allCollectionNames = [];
+        this.allCollections = [];
+        /* eslint-enable no-invalid-this*/
+      };
+
+      testLib = new TestLibDefinition();
+    });
+
+    it('should create a model and attach it to xos lib', () => {
+      define_model(testLib, {
+        urlRoot: 'testUrl',
+        modelName: 'testModel'
+      });
+
+      expect(testLib.allCollectionNames[0]).toEqual('testModels');
+      expect(typeof testLib['testModel']).toBe('function');
+
+    });
+
+    describe('when a model is created', () => {
+      var model;
+      beforeEach(() => {
+        define_model(testLib, {
+          urlRoot: 'testUrl',
+          modelName: 'testModel',
+          // collectionName: 'testCollection',
+          relatedCollections: {instances: 'slices'},
+          foreignCollections: ['sites'],
+          foreignFields: {slice: 'slices'},
+          m2mFields: {sites: 'sites'},
+          listFields: ['name'],
+          detailFields: ['name', 'age'],
+          addFields: ['add'],
+          inputType: {add: 'checkbox'}
+        });
+        /*eslint-disable new-cap*/
+        model = new testLib.testModel();
+        /*eslint-enable new-cap*/
+
+        // add defaults and validator for `testModel`
+        xosdefaults['testModel'] = {name: 'Scott'};
+        xosvalidators['testModel'] = {network_ports: ['portspec']};
+      });
+
+      it('should have a name', () => {
+        expect(model.modelName).toEqual('testModel');
+      });
+
+      it('should have a default collectionName', () => {
+        expect(model.collectionName).toEqual('testModels');
+      });
+
+      describe('whith a custom collectionName', () => {
+        var customCollectionName;
+        beforeEach(() => {
+          define_model(testLib, {
+            urlRoot: 'collUrl',
+            modelName: 'customCollectionName',
+            collectionName: 'myCollection'
+          });
+
+          /*eslint-disable new-cap*/
+          customCollectionName = new testLib.customCollectionName();
+          /*eslint-enable new-cap*/
+        });
+
+        it('should have the custom collectionName', () => {
+          expect(customCollectionName.collectionName).toBe('myCollection');
+        });
+
+        afterEach(() => {
+          customCollectionName = null;
+        });
+      });
+
+      it('should have a valid url', () => {
+        expect(model.url()).toEqual('testUrl/?no_hyperlinks=1');
+      });
+
+      it('should have related collections', () => {
+        expect(model.relatedCollections).toEqual({instances: 'slices'});
+      });
+
+      it('should have foreign collections', () => {
+        expect(model.foreignCollections).toEqual(['sites']);
+      });
+
+      it('should have foreign fields', () => {
+        expect(model.foreignFields).toEqual({slice: 'slices'});
+      });
+
+      it('should have m2m fields', () => {
+        expect(model.m2mFields).toEqual({sites: 'sites'});
+      });
+
+      it('should have list field', () => {
+        expect(model.listFields).toEqual(['name']);
+      });
+
+      it('should have deatil field', () => {
+        expect(model.detailFields).toEqual(['name', 'age']);
+      });
+
+      it('should have add field', () => {
+        expect(model.addFields).toEqual(['add']);
+      });
+
+      it('should have input types defined', () => {
+        expect(model.inputType).toEqual({add: 'checkbox'});
+      });
+
+      it('should have standard defaults', () => {
+        expect(model.defaults).toEqual({name: 'Scott'});
+      });
+
+      describe('when default are defined', () => {
+
+        var extendDefault;
+        beforeEach(() => {
+          define_model(testLib, {
+            urlRoot: 'collUrl',
+            modelName: 'extendDefault',
+            defaults: extend_defaults('testModel', {surname: 'Baker'})
+          });
+
+          /*eslint-disable new-cap*/
+          extendDefault = new testLib.extendDefault();
+          /*eslint-enable new-cap*/
+        });
+
+        it('should return new defaults', () => {
+          expect(extendDefault.defaults).toEqual({name: 'Scott', surname: 'Baker'});
+        });
+
+        afterEach(() => {
+          extendDefault = null;
+        });
+      });
+
+      it('should add default validators', () => {
+        expect(model.validators).toEqual({network_ports: ['portspec']});
+      });
+
+      describe('when validators are defined', () => {
+
+        var extendValidators;
+        beforeEach(() => {
+          define_model(testLib, {
+            urlRoot: 'collUrl',
+            modelName: 'testModel',
+            validators: {site: ['notBlank']}
+          });
+
+          /*eslint-disable new-cap*/
+          extendValidators = new testLib.testModel();
+          /*eslint-enable new-cap*/
+        });
+
+        it('should return extended validators', () => {
+          expect(extendValidators.validators).toEqual({network_ports: ['portspec'], site: ['notBlank']});
+        });
+
+        afterEach(() => {
+          extendValidators = null;
+        });
+      });
+
+      it('should have a xosValidate method', () => {
+        console.log(model.xosValidate);
+        expect(typeof model.xosValidate).toEqual('function');
+      });
+
+      // TBT
+      // - xosValidate
+      // - Test the default
+      // - Test override
+
+    });
+  });
+
   describe('getCookie method with no cookie', () => {
 
     beforeEach(() => {
diff --git a/xos/core/xoslib/static/js/xosTenant.js b/xos/core/xoslib/static/js/xosTenant.js
index d2a3b4e..f933c89 100644
--- a/xos/core/xoslib/static/js/xosTenant.js
+++ b/xos/core/xoslib/static/js/xosTenant.js
@@ -1,456 +1,524 @@
-/* eslint-disable */

 

-XOSTenantSite = XOSModel.extend( {

-    listFields: ["name", "allocated"],

-    modelName: "tenantSite",

-    collectionName: "tenantSites"

+/* globals XOSModel, XOSCollection */

+/* eslint-disable no-undef, guard-for-in, new-cap */

+

+XOSTenantSite = XOSModel.extend({

+  listFields: ['name', 'allocated'],

+  modelName: 'tenantSite',

+  collectionName: 'tenantSites'

 });

 

-XOSTenantSiteCollection = XOSCollection.extend( {

-    listFields: ["name", "allocated", "ready"],

-    modelName: "tenantSite",

-    collectionName: "tenantSites",

+XOSTenantSiteCollection = XOSCollection.extend({

+  listFields: ['name', 'allocated', 'ready'],

+  modelName: 'tenantSite',

+  collectionName: 'tenantSites',

 

-    getFromSlice: function(slice) {

-        var tenantSites = [];

-        var id = 0;

-        for (siteName in slice.attributes.site_allocation) {

-            allocated = slice.attributes.site_allocation[siteName];

-            ready = slice.attributes.site_ready[siteName] || 0;

-            tenantSites.push(new XOSTenantSite( { name: siteName, allocated: allocated, ready: ready, id: id} ));

-            id = id + 1;

-        }

-        for (index in xos.tenantview.models[0].attributes.blessed_site_names) {

-            siteName = xos.tenantview.models[0].attributes.blessed_site_names[index];

-            if (! (siteName in slice.attributes.site_allocation)) {

-                tenantSites.push(new XOSTenantSite( { name: siteName, allocated: 0, ready: 0, id: id} ));

-                id = id + 1;

-            }

-        }

-        this.set(tenantSites);

+  getFromSlice: function(slice) {

+    var tenantSites = [];

+    var id = 0;

+    var that = this;

 

-        var that=this;

-        this.listenTo(slice, 'change', function() { that.getReadyFromSlice(slice); })

-    },

+    for (siteName in slice.attributes.site_allocation) {

+      allocated = slice.attributes.site_allocation[siteName];

+      ready = slice.attributes.site_ready[siteName] || 0;

+      tenantSites.push(new XOSTenantSite({name: siteName, allocated: allocated, ready: ready, id: id}));

+      id = id + 1;

+    }

 

-    getReadyFromSlice: function(slice) {

-        for (siteName in slice.attributes.site_ready) {

-            ready = slice.attributes.site_ready[siteName];

-            for (index in this.models) {

-                tenantSite = this.models[index];

-                if (tenantSite.attributes.name == siteName) {

-                    tenantSite.set("ready", ready);

-                }

-            }

-        }

-    },

+    for (index in xos.tenantview.models[0].attributes.blessed_site_names) {

+      siteName = xos.tenantview.models[0].attributes.blessed_site_names[index];

+      if (! (siteName in slice.attributes.site_allocation)) {

+        tenantSites.push(new XOSTenantSite({name: siteName, allocated: 0, ready: 0, id: id}));

+        id = id + 1;

+      }

+    }

+    this.set(tenantSites);

 

-    putToSlice: function(slice) {

-        slice.attributes.site_allocation = {};

-        for (index in this.models) {

-            var model = this.models[index];

-            slice.attributes.site_allocation[ model.attributes.name ] = model.attributes.allocated;

+    this.listenTo(slice, 'change', function() {

+      that.getReadyFromSlice(slice);

+    });

+  },

+

+  getReadyFromSlice: function(slice) {

+    for (siteName in slice.attributes.site_ready) {

+      ready = slice.attributes.site_ready[siteName];

+      for (index in this.models) {

+        tenantSite = this.models[index];

+        if (tenantSite.attributes.name == siteName) {

+          tenantSite.set('ready', ready);

         }

-    },

+      }

+    }

+  },

+

+  putToSlice: function(slice) {

+    slice.attributes.site_allocation = {};

+    for (index in this.models) {

+      var model = this.models[index];

+

+      slice.attributes.site_allocation[ model.attributes.name ] = model.attributes.allocated;

+    }

+  },

 });

 

 XOSEditUsersView = Marionette.ItemView.extend({

-            template: "#tenant-edit-users",

-            viewInitializers: [],

+  template: '#tenant-edit-users',

+  viewInitializers: [],

 

-            onShow: function() {

-                _.each(this.viewInitializers, function(initializer) {

-                    initializer();

-                });

-            },

+  onShow: function() {

+    _.each(this.viewInitializers, function(initializer) {

+      initializer();

+    });

+  },

 

-            templateHelpers: function() { return { detailView: this, model: this.model }; },

+  templateHelpers: function() {

+    return {detailView: this, model: this.model};

+  }

 

-            });

+});

 

 XOSTenantSummaryView = XOSDetailView.extend({

-            events: {"change": "onChange"},

+  events: {'change': 'onChange'},

 

-            onChange: function(e) {

-                XOSTenantApp.setDirty(true);

-            },

+  onChange: function(e) {

+    XOSTenantApp.setDirty(true);

+  },

 

-            saveSuccess: function() {

-                console.log("saveSuccess!");

-                XOSTenantApp.setDirty(false);

-            },

+  saveSuccess: function() {

+    XOSTenantApp.setDirty(false);

+  },

 

-            });

+});

 

 

 XOSTenantButtonView = Marionette.ItemView.extend({

-            template: "#xos-tenant-buttons-template",

+  template: '#xos-tenant-buttons-template',

 

-            events: {"click button.btn-tenant-create": "createClicked",

-                     "click button.btn-tenant-delete": "deleteClicked",

-                     "click button.btn-tenant-add-user": "addUserClicked",

-                     "click button.btn-tenant-save": "saveClicked",

-                     "click button.btn-tenant-download-ssh": "downloadClicked",

-                     },

+  events: {'click button.btn-tenant-create': 'createClicked',

+           'click button.btn-tenant-delete': 'deleteClicked',

+           'click button.btn-tenant-add-user': 'addUserClicked',

+           'click button.btn-tenant-save': 'saveClicked',

+           'click button.btn-tenant-download-ssh': 'downloadClicked',

+           },

 

-            createClicked: function(e) {

-                     XOSTenantApp.addSlice();

-                     },

+  createClicked: function() {

+    XOSTenantApp.addSlice();

+  },

 

-            deleteClicked: function(e) {

-                     XOSTenantApp.deleteSlice(this.options.linkedView.model);

-                     },

+  deleteClicked: function() {

+    XOSTenantApp.deleteSlice(this.options.linkedView.model);

+  },

 

-            addUserClicked: function(e) {

-                     XOSTenantApp.editUsers(this.options.linkedView.model);

-                     },

+  addUserClicked: function() {

+    XOSTenantApp.editUsers(this.options.linkedView.model);

+  },

 

-            downloadClicked: function(e) {

-                     XOSTenantApp.downloadSSH(this.options.linkedView.model);

-                     },

+  downloadClicked: function() {

+    XOSTenantApp.downloadSSH(this.options.linkedView.model);

+  },

 

-            saveClicked: function(e) {

-                     model = this.options.linkedView.model;

-                     model.tenantSiteCollection.putToSlice(model);

-                     model.attributes.users = model.usersBuffer;

+  saveClicked: function(e) {

+    var model = this.options.linkedView.model;

 

-                     e.preventDefault();

-                     this.options.linkedView.save();

-                     //this.options.linkedView.submitContinueClicked.call(this.options.linkedView, e);

-                     //XOSTenantApp.setDirty(false);

-                     },

-            });

+    model.tenantSiteCollection.putToSlice(model);

+    model.attributes.users = model.usersBuffer;

+

+    e.preventDefault();

+    this.options.linkedView.save();

+    //this.options.linkedView.submitContinueClicked.call(this.options.linkedView, e);

+    //XOSTenantApp.setDirty(false);

+  }

+});

 

 XOSTenantApp = new XOSApplication({

-    logTableId: "#logTable",

-    statusMsgId: "#statusMsg",

-    hideTabsByDefault: true,

-    dirty: false,

-    varName: "XOSTenantApp",

+  logTableId: '#logTable',

+  statusMsgId: '#statusMsg',

+  hideTabsByDefault: true,

+  dirty: false,

+  varName: 'XOSTenantApp',

 });

 

 XOSTenantApp.addRegions({

-    tenantSliceSelector: "#tenantSliceSelector",

-    tenantSummary: "#tenantSummary",

-    tenantSiteList: "#tenantSiteList",

-    tenantButtons: "#tenantButtons",

-    tenantAddSliceInterior: "#tenant-addslice-interior",

-    tenantEditUsersInterior: "#tenant-edit-users-interior",

-    tenantSSHCommandsInterior: "#tenant-ssh-commands-interior",

+  tenantSliceSelector: '#tenantSliceSelector',

+  tenantSummary: '#tenantSummary',

+  tenantSiteList: '#tenantSiteList',

+  tenantButtons: '#tenantButtons',

+  tenantAddSliceInterior: '#tenant-addslice-interior',

+  tenantEditUsersInterior: '#tenant-edit-users-interior',

+  tenantSSHCommandsInterior: '#tenant-ssh-commands-interior',

 });

 

 XOSTenantApp.setDirty = function(dirty) {

-    XOSTenantApp.dirty = dirty;

-    if (dirty) {

-        $("button.btn-tenant-save").addClass("btn-success");

-    } else {

-        $("button.btn-tenant-save").removeClass("btn-success");

-    }

+  XOSTenantApp.dirty = dirty;

+  if (dirty) {

+    $('button.btn-tenant-save').addClass('btn-success');

+  }

+  else {

+    $('button.btn-tenant-save').removeClass('btn-success');

+  }

 };

 

 XOSTenantApp.buildViews = function() {

-     XOSTenantApp.tenantSites = new XOSTenantSiteCollection();

+  XOSTenantApp.tenantSites = new XOSTenantSiteCollection();

 

-     tenantSummaryClass = XOSTenantSummaryView.extend({template: "#xos-detail-template",

-                                                app: XOSTenantApp,

-                                                detailFields: ["serviceClass", "default_image", "default_flavor", "network_ports"],

-                                                fieldDisplayNames: {serviceClass: "Service Level", "default_flavor": "Flavor", "default_image": "Image", "mount_data_sets": "Data Sets"},

-                                                helpText: {"serviceClass": "Existing instances will be re-instantiated if changed",

-                                                           "default_image": "Existing instances will be re-instantiated if changed",

-                                                           "default_flavor": "Existing instances will be re-instantiated if changed"},

+  tenantSummaryClass = XOSTenantSummaryView.extend({

+    template: '#xos-detail-template',

+    app: XOSTenantApp,

+    detailFields: ['serviceClass', 'default_image', 'default_flavor', 'network_ports'],

+    fieldDisplayNames: {

+      serviceClass: 'Service Level',

+      default_flavor: 'Flavor',

+      default_image: 'Image',

+      mount_data_sets: 'Data Sets'

+    },

+    helpText: {

+      'serviceClass': 'Existing instances will be re-instantiated if changed',

+      'default_image': 'Existing instances will be re-instantiated if changed',

+      'default_flavor': 'Existing instances will be re-instantiated if changed'

+    },

+    onShow: function() {

+      // the slice selector is in a different table, so make every label cell the maximal width

+      make_same_width('#xos-tenant-view-panel', '.xos-label-cell');

+    },

+  });

 

-                                                onShow: function() {

-                                                    // the slice selector is in a different table, so make every label cell the maximal width

-                                                    make_same_width("#xos-tenant-view-panel", ".xos-label-cell");

-                                                },

-                                                });

+  XOSTenantApp.tenantSummaryView = tenantSummaryClass;

 

-     XOSTenantApp.tenantSummaryView = tenantSummaryClass;

+  tenantAddClass = XOSDetailView.extend({

+    template: '#xos-detail-template',

+    app: XOSTenantApp,

+    detailFields: ['name', 'description']

+  });

 

-     tenantAddClass = XOSDetailView.extend({template: "#xos-detail-template",

-                                                app: XOSTenantApp,

-                                                detailFields: ["name", "description"]});

+  XOSTenantApp.tenantAddView = tenantAddClass;

 

-     XOSTenantApp.tenantAddView = tenantAddClass;

+  tenantSiteItemClass = XOSItemView.extend({

+    template: '#xos-listitem-template',

+    app: XOSTenantApp

+  });

 

-     tenantSiteItemClass = XOSItemView.extend({template: "#xos-listitem-template",

-                                               app: XOSTenantApp});

+  XOSTenantApp.tenantSiteItemView = tenantSiteItemClass;

 

-     XOSTenantApp.tenantSiteItemView = tenantSiteItemClass;

+  tenantSiteListClass = XOSDataTableView.extend({

+    template: '#xos-list-template',

+    app: XOSTenantApp,

+    childView: tenantSiteItemClass,

+    collection: XOSTenantApp.tenantSites,

+    title: 'sites',

+    inputType: {allocated: 'spinner'},

+    noDeleteColumn: true,

+    disablePaginate: true,

+    disableFilter: true,

+    fieldDisplayNames: {name: 'Site'},

+  });

 

-     tenantSiteListClass = XOSDataTableView.extend({template: "#xos-list-template",

-                                               app: XOSTenantApp,

-                                               childView: tenantSiteItemClass,

-                                               collection: XOSTenantApp.tenantSites,

-                                               title: "sites",

-                                               inputType: {allocated: "spinner"},

-                                               noDeleteColumn: true,

-                                               disablePaginate: true,

-                                               disableFilter: true,

-                                               fieldDisplayNames: {"name": "Site"},

-                                               });

+  XOSTenantApp.tenantSiteListView = tenantSiteListClass;

 

-     XOSTenantApp.tenantSiteListView = tenantSiteListClass;

+  XOSTenantApp.tenantSliceSelectorView = SliceSelectorView.extend({

+    sliceChanged: function(id) {

+      XOSTenantApp.navToSlice(id);

+    },

+    filter: function(slice) {

+      return slice.attributes.current_user_can_see;

+    },

+  });

 

-     XOSTenantApp.tenantSliceSelectorView = SliceSelectorView.extend( {

-         sliceChanged: function(id) {

-             XOSTenantApp.navToSlice(id);

-         },

-         filter: function(slice) {

-             return slice.attributes.current_user_can_see;

-         },

-     });

-

-     xos.sites.fetch();

-     xos.slicesPlus.fetch();

-     xos.tenantview.fetch();

+  xos.sites.fetch();

+  xos.slicesPlus.fetch();

+  xos.tenantview.fetch();

 };

 

 make_choices = function(list_of_names, list_of_values) {

-    result = [];

-    if (!list_of_values) {

-        for (index in list_of_names) {

-            displayName = list_of_names[index];

-            result.push( [displayName, displayName] );

-        }

-    } else {

-        for (index in list_of_names) {

-            displayName = list_of_names[index];

-            id = list_of_values[index];

-            result.push( [displayName, id] );

-        }

+  var result = [];

+  var displayName;

+

+  if (!list_of_values) {

+    for (var index in list_of_names) {

+      displayName = list_of_names[index];

+      result.push([displayName, displayName]);

     }

-    return result;

+  }

+  else {

+    for (var index in list_of_names) {

+      displayName = list_of_names[index];

+      id = list_of_values[index];

+      result.push([displayName, id]);

+    }

+  }

+  return result;

 };

 

 XOSTenantApp.navToSlice = function(id) {

-    XOSTenantApp.viewSlice(xos.slicesPlus.get(id));

+  XOSTenantApp.viewSlice(xos.slicesPlus.get(id));

 };

 

 XOSTenantApp.adjustCollectionField = function(collectionName, id, fieldName, amount) {

-    model = XOSTenantApp[collectionName].get(id);

-    model.set(fieldName, Math.max(model.get(fieldName) + amount, 0));

-    XOSTenantApp.setDirty(true);

+  model = XOSTenantApp[collectionName].get(id);

+  model.set(fieldName, Math.max(model.get(fieldName) + amount, 0));

+  XOSTenantApp.setDirty(true);

 };

 

 XOSTenantApp.addSlice = function() {

-    var app=this;

+  var app = this;

 

-    if (!xos.tenant().current_user_can_create_slice) {

-        window.alert("You do not have sufficient rights to create a slice on your site");

-        return;

+  if (!xos.tenant().current_user_can_create_slice) {

+    window.alert('You do not have sufficient rights to create a slice on your site');

+    return;

+  }

+

+  model = new xos.slicesPlus.model({

+    site: xos.tenant().current_user_site_id,

+    name: xos.tenant().current_user_login_base + '_',

+    creator: xos.tenant().current_user_id

+  });

+

+  var detailView = new XOSTenantApp.tenantAddView({

+    model: model,

+    collection: xos.slicesPlus,

+    noSubmitButton: true,

+  });

+

+  detailView.dialog = $('#tenant-addslice-dialog');

+  app.tenantAddSliceInterior.show(detailView);

+

+  $('#tenant-addslice-dialog').dialog({

+    autoOpen: false,

+    modal: true,

+    width: 640,

+    buttons: {

+      'Create Slice': function() {

+        var addDialog = this;

+

+        detailView.synchronous = true;

+        detailView.afterSave = function() {

+          $(addDialog).dialog('close');

+          XOSTenantApp.navToSlice(detailView.model.id);

+        };

+        detailView.save();

+      },

+      'Cancel': function() {

+        $(this).dialog('close');

+      }

     }

-

-    model = new xos.slicesPlus.model({site: xos.tenant().current_user_site_id,

-                                      name: xos.tenant().current_user_login_base + "_",

-                                      creator: xos.tenant().current_user_id});

-    console.log(model);

-    var detailView = new XOSTenantApp.tenantAddView({model: model,

-                                                     collection: xos.slicesPlus,

-                                                     noSubmitButton: true,

-                                                    });

-    detailView.dialog = $("#tenant-addslice-dialog");

-    app.tenantAddSliceInterior.show(detailView);

-    $("#tenant-addslice-dialog").dialog({

-       autoOpen: false,

-       modal: true,

-       width: 640,

-       buttons : {

-            "Create Slice" : function() {

-              var addDialog = this;

-              console.log("SAVE!!!");

-              detailView.synchronous = true;

-              detailView.afterSave = function() { $(addDialog).dialog("close"); XOSTenantApp.navToSlice(detailView.model.id); }

-              detailView.save();

-            },

-            "Cancel" : function() {

-              $(this).dialog("close");

-            }

-          }

-        });

-    $("#tenant-addslice-dialog").dialog("open");

+  });

+  $('#tenant-addslice-dialog').dialog('open');

 };

 

 XOSTenantApp.editUsers = function(model) {

-    var app=this;

-    var detailView = new XOSEditUsersView({model: model, collection: xos.slicesPlus});

-    detailView.dialog = $("#tenant-edit-users-dialog");

-    app.tenantEditUsersInterior.show(detailView);

-    $("#tenant-edit-users-dialog").dialog({

-       autoOpen: false,

-       modal: true,

-       width: 640,

-       buttons : {

-            "Ok" : function() {

-              var editDialog = this;

-              user_ids = all_options($("#tenant-edit-users-dialog").find(".select-picker-to"));

-              user_ids = user_ids.map( function(x) { return parseInt(x,10); } );

-              if (!array_same_elements(user_ids, model.usersBuffer)) {

-                  XOSTenantApp.setDirty(true);

-              }

-              model.usersBuffer = user_ids;

-              $(editDialog).dialog("close");

-            },

-            "Cancel" : function() {

-              $(this).dialog("close");

-            }

-          }

+  var app = this;

+  var detailView = new XOSEditUsersView({model: model, collection: xos.slicesPlus});

+

+  detailView.dialog = $('#tenant-edit-users-dialog');

+  app.tenantEditUsersInterior.show(detailView);

+

+  $('#tenant-edit-users-dialog').dialog({

+    autoOpen: false,

+    modal: true,

+    width: 640,

+    buttons: {

+      'Ok': function() {

+        var editDialog = this;

+        var user_ids = all_options($('#tenant-edit-users-dialog').find('.select-picker-to'));

+

+        user_ids = user_ids.map(function(x) {

+          return parseInt(x,10);

         });

-    $("#tenant-edit-users-dialog").dialog("open");

+

+        if (!array_same_elements(user_ids, model.usersBuffer)) {

+          XOSTenantApp.setDirty(true);

+        }

+        model.usersBuffer = user_ids;

+        $(editDialog).dialog('close');

+      },

+      'Cancel': function() {

+        $(this).dialog('close');

+      }

+    }

+  });

+  $('#tenant-edit-users-dialog').dialog('open');

 };

 

 XOSTenantApp.downloadSSH = function(model) {

-    var sshCommands = "";

-    for (index in model.attributes.sliceInfo.sshCommands) {

-         sshCommand = model.attributes.sliceInfo.sshCommands[index];

-         sshCommands = sshCommands + sshCommand + "\n";

+  var sshCommands = '';

+

+  for (index in model.attributes.sliceInfo.sshCommands) {

+    sshCommand = model.attributes.sliceInfo.sshCommands[index];

+    sshCommands = sshCommands + sshCommand + '\n';

+  }

+

+  if (sshCommands.length == 0) {

+    alert('this slice has no instantiated instances yet');

+    return;

+  }

+

+  var htmlView = new HTMLView({

+    html: '<pre style="overflow: auto; word-wrap: normal; white-space: pre; word-wrap: normal;">' +

+      sshCommands + '</pre>'

+  });

+

+  XOSTenantApp.tenantSSHCommandsInterior.show(htmlView);

+

+  $('#tenant-ssh-commands-dialog').dialog({

+    autoOpen: false,

+    modal: true,

+    width: 640,

+    buttons: {

+      'Download': function() {

+        var dlLink = document.createElement('a');

+

+        dlLink.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(sshCommands));

+        dlLink.setAttribute('download', 'sshcommands.txt');

+        dlLink.click();

+

+        //window.open('data:text/text,' + encodeURIComponent(sshCommands));

+      },

+      'Close': function() {

+        $(this).dialog('close');

+      },

     }

-

-    if (sshCommands.length == 0) {

-         alert("this slice has no instantiated instances yet");

-         return;

-    }

-

-    var htmlView = new HTMLView({html: '<pre style="overflow: auto; word-wrap: normal; white-space: pre; word-wrap: normal;">' + sshCommands + '</pre>'});

-    XOSTenantApp.tenantSSHCommandsInterior.show(htmlView);

-

-    $("#tenant-ssh-commands-dialog").dialog({

-       autoOpen: false,

-       modal: true,

-       width: 640,

-       buttons : {

-            "Download": function() {

-                var dlLink = document.createElement('a');

-                dlLink.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(sshCommands));

-                dlLink.setAttribute('download', 'sshcommands.txt');

-                dlLink.click();

-

-                //window.open('data:text/text,' + encodeURIComponent(sshCommands));

-            },

-            "Close" : function() {

-              $(this).dialog("close");

-            },

-          }

-        });

-    $("#tenant-ssh-commands-dialog").dialog("open");

+  });

+  $('#tenant-ssh-commands-dialog').dialog('open');

 };

 

 XOSTenantApp.deleteSlice = function(model) {

-    var app=this;

-    app.deleteDialog(model, function() { console.log("afterDelete"); app.viewSlice(undefined); });

+  var app = this;

+

+  app.deleteDialog(model, function() {

+    app.viewSlice(undefined);

+  });

 };

 

 XOSTenantApp.viewSlice = function(model) {

-    if (XOSTenantApp.dirty) {

-        if (!confirm("The current instance has unsaved data -- view new instance anyway ?")) {

-            $("#tenantSliceSelector select").val(XOSTenantApp.currentSlice.id);

-            return;

-        }

+  if (XOSTenantApp.dirty) {

+    if (!confirm('The current instance has unsaved data -- view new instance anyway ?')) {

+      $('#tenantSliceSelector select').val(XOSTenantApp.currentSlice.id);

+      return;

     }

+  }

 

-    XOSTenantApp.setDirty(false);

+  XOSTenantApp.setDirty(false);

 

-    if (!model && xos.slicesPlus.models.length > 0) {

-        model = xos.slicesPlus.models[0];

-    }

+  if (!model && xos.slicesPlus.models.length > 0) {

+    model = xos.slicesPlus.models[0];

+  }

 

-    if (model) {

-        sliceSelector = new XOSTenantApp.tenantSliceSelectorView({collection: xos.slicesPlus,

-                                                                  selectedID: model ? model.id : null,

-                                                                 } );

-        XOSTenantApp.sliceSelector = sliceSelector;

-        XOSTenantApp.tenantSliceSelector.show(sliceSelector);

+  if (model) {

+    sliceSelector = new XOSTenantApp.tenantSliceSelectorView({

+      collection: xos.slicesPlus,

+      selectedID: model ? model.id : null,

+    });

 

-        tenantSummary = new XOSTenantApp.tenantSummaryView({model: model,

-                                                            choices: {mount_data_sets: make_choices(xos.tenant().public_volume_names, null),

-                                                                      serviceClass: make_choices(xos.tenant().blessed_service_class_names, xos.tenant().blessed_service_classes),

-                                                                      default_image: make_choices(xos.tenant().blessed_image_names, xos.tenant().blessed_images),

-                                                                      default_flavor: make_choices(xos.tenant().blessed_flavor_names, xos.tenant().blessed_flavors),},

-                                                           });

-        XOSTenantApp.tenantSummary.show(tenantSummary);

+    XOSTenantApp.sliceSelector = sliceSelector;

+    XOSTenantApp.tenantSliceSelector.show(sliceSelector);

 

-        tenantSites = new XOSTenantSiteCollection();

-        tenantSites.getFromSlice(model);

-        model.usersBuffer = model.attributes.users; /* save a copy of 'users' that we can edit. This prevents another view (developer) from overwriting our copy with a fetch from the server */

-        model.usersOrig = model.attributes.users;   /* save an immutable copy that we'll use for username lookups */

-        model.user_namesOrig = model.attributes.user_names;

-        model.tenantSiteCollection = tenantSites;

-        XOSTenantApp.tenantSites = tenantSites;

+    tenantSummary = new XOSTenantApp.tenantSummaryView({

+      model: model,

+      choices: {

+        mount_data_sets: make_choices(xos.tenant().public_volume_names, null),

+        serviceClass: make_choices(xos.tenant().blessed_service_class_names, xos.tenant().blessed_service_classes),

+        default_image: make_choices(xos.tenant().blessed_image_names, xos.tenant().blessed_images),

+        default_flavor: make_choices(xos.tenant().blessed_flavor_names, xos.tenant().blessed_flavors)

+      },

+    });

 

-        tenantSiteList = new XOSTenantApp.tenantSiteListView({collection: tenantSites });

-        XOSTenantApp.tenantSiteList.show(tenantSiteList);

-        // on xos.slicePlus.sort, need to update xostenantapp.tenantSites

+    XOSTenantApp.tenantSummary.show(tenantSummary);

 

-        XOSTenantApp.tenantButtons.show( new XOSTenantButtonView( { app: XOSTenantApp,

-                                                                    linkedView: tenantSummary } ) );

+    tenantSites = new XOSTenantSiteCollection();

+    tenantSites.getFromSlice(model);

+    model.usersBuffer = model.attributes.users; /* save a copy of 'users' that we can edit. This prevents another view (developer) from overwriting our copy with a fetch from the server */

+    model.usersOrig = model.attributes.users;   /* save an immutable copy that we'll use for username lookups */

+    model.user_namesOrig = model.attributes.user_names;

+    model.tenantSiteCollection = tenantSites;

+    XOSTenantApp.tenantSites = tenantSites;

 

-        XOSTenantApp.currentSlice = model;

-    } else {

-        XOSTenantApp.tenantSliceSelector.show(new HTMLView({html: ""}));

-        XOSTenantApp.tenantSummary.show(new HTMLView({html: "You have no slices"}));

-        XOSTenantApp.tenantSiteList.show(new HTMLView({html: ""}));

-        XOSTenantApp.tenantButtons.show( new XOSTenantButtonView( { template: "#xos-tenant-buttons-noslice-template",

-                                                                    app: XOSTenantApp,

-                                                                    linkedView: tenantSummary } ) );

-    }

+    tenantSiteList = new XOSTenantApp.tenantSiteListView({collection: tenantSites});

+    XOSTenantApp.tenantSiteList.show(tenantSiteList);

+    // on xos.slicePlus.sort, need to update xostenantapp.tenantSites

+

+    XOSTenantApp.tenantButtons.show(

+      new XOSTenantButtonView({

+        app: XOSTenantApp,

+        linkedView: tenantSummary

+      })

+    );

+

+    XOSTenantApp.currentSlice = model;

+  }

+  else {

+    XOSTenantApp.tenantSliceSelector.show(new HTMLView({html: ''}));

+    XOSTenantApp.tenantSummary.show(new HTMLView({html: 'You have no slices'}));

+    XOSTenantApp.tenantSiteList.show(new HTMLView({html: ''}));

+    XOSTenantApp.tenantButtons.show(

+      new XOSTenantButtonView({

+        template: '#xos-tenant-buttons-noslice-template',

+        app: XOSTenantApp,

+        linkedView: tenantSummary

+      })

+    );

+  }

 };

 

 XOSTenantApp.sanityCheck = function() {

-    errors = [];

-    if (xos.tenant().blessed_deployments && xos.tenant().blessed_deployments.length == 0) {

-        errors.push("no blessed deployments");

-    }

-    if (xos.tenant().blessed_service_classes.length == 0) {

-        errors.push("no blessed service classes");

-    }

-    if (xos.tenant().blessed_flavors.length == 0) {

-        errors.push("no blessed flavors");

-    }

-    if (xos.tenant().blessed_images.length == 0) {

-        errors.push("no blessed images");

-    }

-    if (xos.tenant().blessed_sites.length == 0) {

-        errors.push("no blessed sites");

-    }

-    if (xos.tenant().current_user_site_id == null) {

-        errors.push("current user does not have a site");

-    }

+  errors = [];

+  if (xos.tenant().blessed_deployments && xos.tenant().blessed_deployments.length == 0) {

+    errors.push('no blessed deployments');

+  }

+  if (xos.tenant().blessed_service_classes.length == 0) {

+    errors.push('no blessed service classes');

+  }

+  if (xos.tenant().blessed_flavors.length == 0) {

+    errors.push('no blessed flavors');

+  }

+  if (xos.tenant().blessed_images.length == 0) {

+    errors.push('no blessed images');

+  }

+  if (xos.tenant().blessed_sites.length == 0) {

+    errors.push('no blessed sites');

+  }

+  if (xos.tenant().current_user_site_id == null) {

+    errors.push('current user does not have a site');

+  }

 

-    if (errors.length > 0) {

-         t = templateFromId("#tenant-sanity-check")

-         $("#tenantSummary").html( t({errors: errors, blessed_deployment_names: xos.tenant().blessed_deployment_names}) );

-         return false;

-    }

+  if (errors.length > 0) {

+    t = templateFromId('#tenant-sanity-check');

+    $('#tenantSummary').html(t({

+      errors: errors,

+      blessed_deployment_names:

+      xos.tenant().blessed_deployment_names

+    }));

+    return false;

+  }

 

-    return true;

-}

-

-XOSTenantApp.collectionLoadChange = function() {

-    stats = xos.getCollectionStatus();

-

-    if (!XOSTenantApp.navigationStarted) {

-        if (stats["isLoaded"] + stats["failedLoad"] >= stats["startedLoad"]) {

-            if (XOSTenantApp.sanityCheck()) {

-                XOSTenantApp.viewSlice(undefined);

-            }

-        } else {

-            $("#tenantSummary").html("<h3>Loading...</h3><div id='xos-startup-progress'></div>");

-            $("#xos-startup-progress").progressbar({value: stats["completedLoad"], max: stats["startedLoad"]});

-        }

-    }

+  return true;

 };

 

-XOSTenantApp.on("start", function() {

-     XOSTenantApp.buildViews();

+XOSTenantApp.collectionLoadChange = function() {

+  stats = xos.getCollectionStatus();

 

-     // fire it once to initially show the progress bar

-     XOSTenantApp.collectionLoadChange();

+  if (!XOSTenantApp.navigationStarted) {

+    if (stats['isLoaded'] + stats['failedLoad'] >= stats['startedLoad']) {

+      if (XOSTenantApp.sanityCheck()) {

+        XOSTenantApp.viewSlice(undefined);

+      }

+    }

+    else {

+      $('#tenantSummary').html('<h3>Loading...</h3><div id="xos-startup-progress"></div>');

+      $('#xos-startup-progress').progressbar({value: stats['completedLoad'], max: stats['startedLoad']});

+    }

+  }

+};

 

-     // fire it each time the collection load status is updated

-     Backbone.on("xoslib:collectionLoadChange", XOSTenantApp.collectionLoadChange);

+XOSTenantApp.on('start', function() {

+  XOSTenantApp.buildViews();

+

+  // fire it once to initially show the progress bar

+  XOSTenantApp.collectionLoadChange();

+

+  // fire it each time the collection load status is updated

+  Backbone.on('xoslib:collectionLoadChange', XOSTenantApp.collectionLoadChange);

 });

 

-$(document).ready(function(){

-    XOSTenantApp.start();

+$(document).ready(function() {

+  XOSTenantApp.start();

 });

 /* eslint-enable */

diff --git a/xos/core/xoslib/static/js/xoslib/xos-backbone.js b/xos/core/xoslib/static/js/xoslib/xos-backbone.js
index 6fb6cc1..a677d88 100644
--- a/xos/core/xoslib/static/js/xoslib/xos-backbone.js
+++ b/xos/core/xoslib/static/js/xoslib/xos-backbone.js
@@ -1,5 +1,5 @@
-/* eslint-disable*/
 
+/* eslint-disable*/
 if (! window.XOSLIB_LOADED) {
     window.XOSLIB_LOADED=true;
 
@@ -134,6 +134,7 @@
         xosValidate: function(attrs, options) {
             errors = {};
             foundErrors = false;
+
             _.each(this.validators, function(validatorList, fieldName) {
                 _.each(validatorList, function(validator) {
                     if (fieldName in attrs) {
@@ -385,7 +386,50 @@
         }
     }
 
+    /**
+    * This is an helper function to define XOS model.
+    *
+    * @param {object} lib A backbone collection library (eg: xos)
+    * @param {object} attrs The model attributes
+    * @param {string} attrs.modelName The name of the model
+    * @param {string} attrs.urlRoot The base url for the collection
+    * @param {object} [attrs.relatedCollections] collections which should be drawn as an inline 
+                                                 list when the detail view is displayed. 
+                                                 Format: **collection:collectionFieldName** 
+                                                 where **collectionFieldName** is the name of the field 
+                                                 in the collection that points back to the collection 
+                                                 in the detail view.
+    * @param {array} [attrs.foreignCollections] collections which are used in idToName() calls
+                                when presenting the data to the user. Used to
+                                create a listento event. Somewhat
+                                redundant with foreignFields.
+    * @param {object} [attrs.foreignFields] **localFieldName:collection**. Used to
+                                automatically map ids into humanReadableNames
+                                when presenting data to the user.
+    * @param {object} [attrs.m2mFields] **localFieldName:collection**. Used to
+                                populate choices in picker lists. Similar to
+                                foreignFields.
+    * @param {Array} [attrs.listFields] Fields to display in lists
+    * @param {Array} {attrs.detailFields} Fields to display in detail views
+    * @param {Array} [attrs.addFields] Fields to display in popup add windows
+    * @param {Object} [attrs.inputType] by default, "detailFields" will be displayed
+                                as text input controls. This will let you display
+                                a checkbox or a picker instead.
+    * @param {Object} [attrs.defaults] Override the model defaults, `extend_defaults` can be used.
+    * @param {Object} [attrs.validators] Add validators to this model. Use `validateField` method in xos-util.js
+    * @param {function} [attrs.preSave] A pre-save method
+    * @param {function} [attrs.xosValidate] Override the default xosValidate.
+    *                                       If you want to call it either start the function with
+    *                                       `errors = XOSModel.prototype.xosValidate.call(this, attrs, options);`
+    * @returns void
+    */
+
     function define_model(lib, attrs) {
+
+        // NOTE shouldn't we trhow an error if no:
+        // - attrs.urlRoot
+        // - attrs.modelName
+
         modelName = attrs.modelName;
         modelClassName = modelName;
         collectionClass = attrs.collectionClass || XOSCollection;
@@ -397,6 +441,8 @@
 
         attrs.inputType = attrs.inputType || {};
         attrs.foreignFields = attrs.foreignFields || {};
+        // NOTE m2mFields are not set in modelAttr,
+        // see list in for loop
         attrs.m2mFields = attrs.m2mFields || {};
         attrs.readOnlyFields = attrs.readOnlyFields || [];
         attrs.detailLinkFields = attrs.detailLinkFields || ["id","name"];
@@ -415,7 +461,9 @@
                 modelAttrs[key] = value;
                 collectionAttrs[key] = value;
             }
-            if ($.inArray(key, ["validate", "preSave", "readOnlyFields"]) >= 0) {
+            // NOTE xosValidate added by Matteo Scandolo
+            // check with Scott
+            if ($.inArray(key, ["validate", "preSave", "readOnlyFields", "xosValidate"]) >= 0) {
                 modelAttrs[key] = value;
             }
         }
@@ -424,17 +472,18 @@
             modelAttrs.defaults = get_defaults(modelName);
         }
 
-//        if ((typeof xosdefaults !== "undefined") && xosdefaults[modelName]) {
-//            modelAttrs["defaults"] = xosdefaults[modelName];
-//        }
-
+        // if there are default validators for this model
+        // extend with customs
         if ((typeof xosvalidators !== "undefined") && xosvalidators[modelName]) {
             modelAttrs["validators"] = $.extend({}, xosvalidators[modelName], attrs["validators"] || {});
-        } else if (attrs["validators"]) {
-            modelAttrs["validators"] = attrs["validators"];
-            console.log(attrs);
-            console.log(modelAttrs);
         }
+        // else use custom
+        else if (attrs["validators"]) {
+            modelAttrs["validators"] = attrs["validators"];
+            // console.log(attrs);
+            // console.log(modelAttrs);
+        }
+        // NOTE Why define validators in multiple places?
 
         lib[modelName] = XOSModel.extend(modelAttrs);
 
@@ -737,14 +786,15 @@
                             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;
+
+                                var site = xos.tenant().current_user_login_base;
                                 if ((site!=undefined) && (attrs.name!=undefined)) {
-                                    site = xos.sites.get(site);
-                                    if (attrs.name.indexOf(site.attributes.login_base+"_") != 0) {
+                                    if (attrs.name.indexOf(site + "_") < 0) {
                                         errors = errors || {};
-                                        errors["name"] = "must start with " + site.attributes.login_base + "_";
+                                        errors["name"] = "must start with " + site + "_";
                                     }
                                 }
+
                                 return errors;
                             },
                         });
diff --git a/xos/core/xoslib/static/js/xoslib/xos-defaults.js b/xos/core/xoslib/static/js/xoslib/xos-defaults.js
index 0f4ab27..06ee0d9 100644
--- a/xos/core/xoslib/static/js/xoslib/xos-defaults.js
+++ b/xos/core/xoslib/static/js/xoslib/xos-defaults.js
@@ -1,5 +1,6 @@
 /* eslint-disable quotes, no-undef, max-len, new-cap*/
 /* eslint indent: [2, 2]*/
+console.warn('**** XOS DEFAULT ****');
 function xos_get_defaults() {
   this.account = {"updated": null, "policed": null, "created": null, "deleted": false, "site": null, "lazy_blocked": false, "backend_register": "{}", "write_protect": false, "backend_status": "0 - Provisioning in progress", "no_sync": false, "enacted": null};
   this.charge = {"updated": null, "slice": null, "date": null, "policed": null, "created": null, "deleted": false, "object": null, "account": null, "lazy_blocked": false, "backend_register": "{}", "write_protect": false, "amount": 0.0, "state": "pending", "invoice": null, "coreHours": 0.0, "backend_status": "0 - Provisioning in progress", "kind": "besteffort", "no_sync": false, "enacted": null};
diff --git a/xos/core/xoslib/static/js/xoslib/xos-validators.js b/xos/core/xoslib/static/js/xoslib/xos-validators.js
index ecee144..f762ef2 100644
--- a/xos/core/xoslib/static/js/xoslib/xos-validators.js
+++ b/xos/core/xoslib/static/js/xoslib/xos-validators.js
@@ -1,3 +1,9 @@
+/**
+* List of model validator
+*
+* @constructor
+*/
+
 /* eslint-disable quotes, no-undef, max-len, new-cap*/
 /* eslint indent: [2, 2]*/
 function xos_get_validators() {
diff --git a/xos/core/xoslib/static/js/xoslib/xosHelper.js b/xos/core/xoslib/static/js/xoslib/xosHelper.js
index cad242f..16046ae 100644
--- a/xos/core/xoslib/static/js/xoslib/xosHelper.js
+++ b/xos/core/xoslib/static/js/xoslib/xosHelper.js
@@ -573,7 +573,9 @@
                    model.save, we call it ourselves, so we can throw up our
                    validation error before creating the infoMsg in the log
                 */
+               
                 errors =  this.model.xosValidate(data);
+
                 if (errors) {
                     this.onFormDataInvalid(errors);
                     return;