tenant view, WIP
diff --git a/planetstack/core/xoslib/dashboards/xosTenant.html b/planetstack/core/xoslib/dashboards/xosTenant.html
new file mode 100644
index 0000000..49236d4
--- /dev/null
+++ b/planetstack/core/xoslib/dashboards/xosTenant.html
@@ -0,0 +1,72 @@
+<script src="{{ STATIC_URL }}/js/vendor/underscore-min.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.syphon.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.wreqr.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.babysitter.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.marionette.js"></script>
+
+<link rel="stylesheet" href="//code.jquery.com/ui/1.11.2/themes/smoothness/jquery-ui.css">
+<link rel="stylesheet" type="text/css" href="{% static 'css/xosAdminDashboard.css' %}" media="all" >
+<link rel="stylesheet" type="text/css" href="{% static 'css/xosAdminSite.css' %}" media="all" >
+
+<script src="{{ STATIC_URL }}/js/xoslib/xos-util.js"></script>
+<script src="{{ STATIC_URL }}/js/xoslib/xos-defaults.js"></script>
+<script src="{{ STATIC_URL }}/js/xoslib/xos-validators.js"></script>
+<script src="{{ STATIC_URL }}/js/xoslib/xos-backbone.js"></script>
+<script src="{{ STATIC_URL }}/js/xoslib/xosHelper.js"></script>
+<script src="{{ STATIC_URL }}/js/xosTenant.js"></script>
+
+<script type="text/template" id="xos-tenant-buttons-template">
+  <div class="box save-box">
+    <button class="btn btn-high btn-tenant-create">Create New Slice</button>
+    <button class="btn btn-high btn-tenant-delete">Delete Slice</button>
+    <button class="btn btn-high btn-tenant-add-user">Add User</button>
+    <button class="btn btn-high btn-tenant-save">Save</button>
+  </div>
+</script>
+
+<script type="text/template" id="xos-log-template">
+  <tr id="<%= logMessageId %>" class="xos-log xos-<%= statusclass %>">
+     <td><%= what %><br>
+         <%= status %> <%= statusText %>
+     </td>
+  </tr>
+</script>
+
+<div id="xos-confirm-dialog" title="Confirmation Required">
+  Are you sure about this?

+</div>
+
+<div id="xos-error-dialog" title="Error Message">
+</div>

+

+<div id="contentPanel">

+<div id="contentTitle">
+</div>
+<div id="contentButtonPanel">
+
+<div id="rightButtonPanel"></div>
+
+<div class="box" id="logPanel">
+<table id="logTable">
+<tbody>
+</tbody>
+</table> <!-- end logTable -->
+</div> <!-- end logPanel -->
+</div> <!-- end contentButtonPanel -->
+
+<div id="contentInner">
+
+<div id="tenantSliceSelector">
+</div>
+<div id="tenantSummary">
+</div>
+<div id="tenantSiteList">
+</div>
+<div id="tenantButtons">
+</div>
+
+</div> <!-- end contentInner -->
+</div> <!-- end contentPanel -->
+
+{% include 'xosAdmin.html' %}
diff --git a/planetstack/core/xoslib/static/js/picker.js b/planetstack/core/xoslib/static/js/picker.js
index 0302cf4..075bdc5 100644
--- a/planetstack/core/xoslib/static/js/picker.js
+++ b/planetstack/core/xoslib/static/js/picker.js
@@ -48,3 +48,7 @@
         });

     });

 };

+

+function init_spinner(selector, value) {

+     var spinner = $(selector).spinner( "value", value);

+};

diff --git a/planetstack/core/xoslib/static/js/xosTenant.js b/planetstack/core/xoslib/static/js/xosTenant.js
new file mode 100644
index 0000000..5095505
--- /dev/null
+++ b/planetstack/core/xoslib/static/js/xosTenant.js
@@ -0,0 +1,218 @@
+XOSTenantSite = XOSModel.extend( {
+    listFields: ["name", "allocated"],
+    modelName: "tenantSite",
+    collectionName: "tenantSites"
+});
+
+XOSTenantSiteCollection = XOSCollection.extend( {
+    listFields: ["name", "allocated"],
+    modelName: "tenantSite",
+    collectionName: "tenantSites",
+
+    updateFromSlice: function(slice) {
+        var tenantSites = [];
+        var id = 0;
+        for (siteName in slice.attributes.site_allocation) {
+            allocated = slice.attributes.site_allocation[siteName];
+            tenantSites.push(new XOSTenantSite( { name: siteName, allocated: allocated, 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, id: id} ));
+                id = id + 1;
+            }
+        }
+        this.set(tenantSites);
+    },
+});
+
+XOSTenantButtonView = Marionette.ItemView.extend({
+            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",
+                     },
+
+            createClicked: function(e) {
+                     },
+
+            deleteClicked: function(e) {
+                     this.options.linkedView.deleteClicked.call(this.options.linkedView, e);
+                     },
+
+            addUserClicked: function(e) {
+                     },
+
+            saveClicked: function(e) {
+                     this.options.linkedView.submitContinueClicked.call(this.options.linkedView, e);
+                     },
+            });
+
+XOSTenantApp = new XOSApplication({
+    logTableId: "#logTable",
+    statusMsgId: "#statusMsg",
+    hideTabsByDefault: true
+});
+
+XOSTenantApp.addRegions({
+    tenantSliceSelector: "#tenantSliceSelector",
+    tenantSummary: "#tenantSummary",
+    tenantSiteList: "#tenantSiteList",
+    tenantButtons: "#tenantButtons",
+});
+
+XOSTenantApp.buildViews = function() {

+     XOSTenantApp.tenantSites = new XOSTenantSiteCollection();

+

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

+                                                app: XOSTenantApp,

+                                                detailFields: ["serviceClass", "image_preference", "network_ports", "mount_data_sets"]});

+

+     XOSTenantApp.tenantSummaryView = tenantSummaryClass;

+

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

+                                               app: XOSTenantApp});

+

+     XOSTenantApp.tenantSiteItemView = tenantSiteItemClass;

+

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

+                                               app: XOSTenantApp,

+                                               childView: tenantSiteItemClass,

+                                               collection: XOSTenantApp.tenantSites,

+                                               title: "sites",

+                                               inputType: {allocated: "spinner"},

+                                               noDeleteColumn: true,

+                                               });

+

+     XOSTenantApp.tenantSiteListView = tenantSiteListClass;

+

+     XOSTenantApp.tenantSliceSelectorView = SliceSelectorView.extend( {

+         sliceChanged: function(id) {

+             //console.log("navigate to " + id);

+             XOSTenantApp.Router.navigate("slice/" + id, {trigger: true});

+         },

+     });

+

+     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] );

+        }

+    }

+    return result;

+};

+

+XOSTenantApp.viewSlice = function(model) {

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

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

+    }

+

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

+                                                              selectedID: model.id,

+                                                             } );

+    XOSTenantApp.sliceSelector = sliceSelector;

+    XOSTenantApp.tenantSliceSelector.show(sliceSelector);

+

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

+                                                        choices: {mount_data_sets: make_choices(xos.tenantview.models[0].attributes.public_volume_names, null),

+                                                                  image_preference: make_choices(xos.tenantview.models[0].attributes.blessed_image_names, null)},

+                                                       });

+    XOSTenantApp.tenantSummary.show(tenantSummary);

+

+    tenantSites = new XOSTenantSiteCollection();

+    tenantSites.updateFromSlice(model);

+    XOSTenantApp.tenantSites = tenantSites;

+

+    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.initRouter = function() {

+    router = XOSRouter;

+    var api = {};

+    var routes = {};

+

+    nav_url = "slice/:id";

+    api_command = "viewSlice";

+    api[api_command] = function(id) { XOSTenantApp.viewSlice(xos.slicesPlus.get(id)); };

+    routes[nav_url] = api_command;

+

+    nav_url = "increase/:collectionName/:id/:fieldName";

+    api_command = "increase";

+    api[api_command] = function(collectionName, id, fieldName) {

+                           XOSTenantApp.Router.showPreviousURL();

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

+                           model.set(fieldName, model.get(fieldName) + 1);

+                       };

+    routes[nav_url] = api_command;

+

+    nav_url = "decrease/:collectionName/:id/:fieldName";

+    api_command = "decrease";

+    api[api_command] = function(collectionName, id, fieldName) {

+                           XOSTenantApp.Router.showPreviousURL();

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

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

+                       };

+    routes[nav_url] = api_command;

+

+    nav_url = "*path";

+    api_command = "defaultRoute";

+    api[api_command] = function() { XOSTenantApp.viewSlice(undefined); };

+    routes[nav_url] = api_command;

+

+    XOSTenantApp.Router = new router({ appRoutes: routes, controller: api });

+};

+

+XOSTenantApp.startNavigation = function() {

+    Backbone.history.start();

+    XOSTenantApp.navigationStarted = true;

+}

+

+XOSTenantApp.collectionLoadChange = function() {

+    stats = xos.getCollectionStatus();

+

+    if (!XOSTenantApp.navigationStarted) {

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

+            XOSTenantApp.startNavigation();

+

+            //if (xos.slicesPlus.models.length > 0) {

+            //    XOSTenantApp.Router.navigate("slice/" + xos.slicesPlus.models[0].id, {trigger:true});

+            //}

+        } else {

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

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

+        }

+    }

+};

+

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

+     XOSTenantApp.buildViews();
+
+     XOSTenantApp.initRouter();
+
+     // 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();
+});
+
diff --git a/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js b/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js
index 47ea66a..762a2b5 100644
--- a/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js
+++ b/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js
@@ -34,8 +34,16 @@
     USERDEPLOYMENT_API = "/plstackapi/userdeployments/";
 
     SLICEPLUS_API = "/xoslib/slicesplus/";
+    TENANTVIEW_API = "/xoslib/tenantview/"
 
     XOSModel = Backbone.Model.extend({
+        relatedCollections: [],
+        foreignCollections: [],
+        foreignFields: {},
+        m2mFields: {},
+        readonlyFields: [],
+        detailLinkFields: [],
+
         /* from backbone-tastypie.js */
         //idAttribute: 'resource_uri',
 
@@ -152,6 +160,10 @@
 

         relatedCollections: [],

         foreignCollections: [],

+        foreignFields: {},

+        m2mFields: {},
+        readonlyFields: [],
+        detailLinkFields: [],

 

         sorted: function() {

             //console.log("sorted " + this.modelName);

@@ -333,6 +345,22 @@
             },
     });
 
+    function get_defaults(modelName) {
+        if ((typeof xosdefaults !== "undefined") && xosdefaults[modelName]) {
+            return xosdefaults[modelName];
+        }
+        return undefined;
+    }
+
+    function extend_defaults(modelName, stuff) {
+        defaults = get_defaults(modelName);
+        if (defaults) {
+            return $.extend({}, defaults, stuff);
+        } else {
+            return stuff;
+        }
+    }
+
     function define_model(lib, attrs) {
         modelName = attrs.modelName;
         modelClassName = modelName;
@@ -358,7 +386,7 @@
 
         for (key in attrs) {
             value = attrs[key];
-            if ($.inArray(key, ["urlRoot", "modelName", "collectionName", "listFields", "addFields", "detailFields", "detailLinkFields", "foreignFields", "inputType", "relatedCollections", "foreignCollections"])>=0) {
+            if ($.inArray(key, ["urlRoot", "modelName", "collectionName", "listFields", "addFields", "detailFields", "detailLinkFields", "foreignFields", "inputType", "relatedCollections", "foreignCollections", "defaults"])>=0) {
                 modelAttrs[key] = value;
                 collectionAttrs[key] = value;
             }
@@ -367,9 +395,15 @@
             }
         }
 
-        if ((typeof xosdefaults !== "undefined") && xosdefaults[modelName]) {
-            modelAttrs["defaults"] = xosdefaults[modelName];
+        if (!modelAttrs.defaults) {
+            modelAttrs.defaults = get_defaults(modelName);
         }
+        console.log(modelName);
+        console.log(modelAttrs);
+
+//        if ((typeof xosdefaults !== "undefined") && xosdefaults[modelName]) {
+//            modelAttrs["defaults"] = xosdefaults[modelName];
+//        }
 
         if ((typeof xosvalidators !== "undefined") && xosvalidators[modelName]) {
             modelAttrs["validators"] = xosvalidators[modelName];
@@ -629,9 +663,36 @@
         // enhanced REST
         // XXX this really needs to somehow be combined with Slice, to avoid duplication
         define_model(this, {urlRoot: SLICEPLUS_API,
-                            relatedCollections: {'slivers': "slice"},
-                            modelName: "slicePlus",
-                            collectionName: "slicesPlus"});
+                           relatedCollections: {"slivers": "slice", "slicePrivileges": "slice", "networks": "owner"},
+                           foreignCollections: ["services", "sites"],
+                           foreignFields: {"service": "services", "site": "sites"},
+                           listFields: ["backend_status", "id", "name", "enabled", "description", "slice_url", "site", "max_slivers", "service"],
+                           detailFields: ["backend_status", "name", "site", "enabled", "description", "slice_url", "max_slivers"],
+                           inputType: {"enabled": "checkbox"},
+                           modelName: "slicePlus",
+                           collectionName: "slicesPlus",
+                           defaults: extend_defaults("slice", {"network_ports": "", "site_allocation": []}),
+                           xosValidate: function(attrs, options) {
+                               errors = XOSModel.prototype.xosValidate(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;
+                             },
+                           });
+
+        define_model(this, {urlRoot: TENANTVIEW_API,
+                            modelName: "tenantview",
+                            collectionName: "tenantview",
+                            listFields: [],
+                            detailFields: [],
+                            });
 
         this.listObjects = function() { return this.allCollectionNames; };
 
diff --git a/planetstack/core/xoslib/static/js/xoslib/xosHelper.js b/planetstack/core/xoslib/static/js/xoslib/xosHelper.js
index 6c0c2f3..0cadf79 100644
--- a/planetstack/core/xoslib/static/js/xoslib/xosHelper.js
+++ b/planetstack/core/xoslib/static/js/xoslib/xosHelper.js
@@ -4,6 +4,41 @@
   },
 });
 
+SliceSelectorOption = Marionette.ItemView.extend({
+    template: "#xos-sliceselector-option",
+    tagName: "option",
+    attributes: function() {
+        console.log("XXX");
+        console.log(this.options.selectedID);
+        console.log(this.model.get("id"));
+        if (this.options.selectedID == this.model.get("id")) {
+            return { value: this.model.get("id"), selected: 1 };
+        } else {
+            return { value: this.model.get("id") };
+        }
+    },
+});
+
+SliceSelectorView = Marionette.CompositeView.extend({
+    template: "#xos-sliceselector-select",
+    childViewContainer: "select",
+    childView: SliceSelectorOption,
+
+    events: {"change select": "onSliceChanged"},
+
+    childViewOptions: function() {
+        return { selectedID: this.options.selectedID || this.selectedID || null };
+    },
+
+    onSliceChanged: function() {
+        this.sliceChanged(this.$el.find("select").val());
+    },
+
+    sliceChanged: function(id) {
+        console.log("sliceChanged " + id);
+    },
+});
+
 FilteredCompositeView = Marionette.CompositeView.extend( {
     showCollection: function() {
       var ChildView;
@@ -25,6 +60,7 @@
 

         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() {

@@ -33,8 +69,8 @@
 
         showPreviousURL: function() {
             prevPage = this.prevPage();
-            console.log("showPreviousURL");
-            console.log(this.routeStack);
+            //console.log("showPreviousURL");
+            //console.log(this.routeStack);
             if (prevPage) {
                 this.navigate("#"+prevPage, {trigger: false, replace: true} );
             }
@@ -103,6 +139,8 @@
             parsed_error=undefined;
             width=640;    // django stacktraces like wide width
         }
+        console.log(responseText);
+        console.log(parsed_error);
         if (parsed_error) {
             $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
         } else {
@@ -392,7 +430,7 @@
                      },
 
             submitDeleteClicked: function(e) {
-                     this.options.linkedView.submitDeleteClicked.call(this.options.linkedView, e);
+                     this.options.linkedView.deleteClicked.call(this.options.linkedView, e);
                      },
 
             addClicked: function(e) {
@@ -501,13 +539,13 @@
                 }
 
                 if (isNew) {
-                    this.model.attributes.humanReadableName = "new " + model.modelName;
+                    this.model.attributes.humanReadableName = "new " + this.model.modelName;
                     this.model.addToCollection = this.collection;
                 } else {
                     this.model.addToCollection = undefined;
                 }
 
-                var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
+                var infoMsgId = this.app.showInformational( {what: "save " + this.model.modelName + " " + this.model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
 
                 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
                                        success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);
@@ -609,12 +647,13 @@
                                                     collectionName: this.model.collectionName,
                                                     addFields: this.model.addFields,
                                                     listFields: this.model.listFields,
-                                                    detailFields: this.model.detailFields,
+                                                    detailFields: this.options.detailFields || this.detailFields || this.model.detailFields,
                                                     foreignFields: this.model.foreignFields,
                                                     detailLinkFields: this.model.detailLinkFields,
                                                     inputType: this.model.inputType,
                                                     model: this.model,
                                                     detailView: this,
+                                                    choices: this.options.choices || this.choices || this.model.choices || {},
                                          }},
 });
 
@@ -829,6 +868,7 @@
         view.columnsByIndex = [];
         view.columnsByFieldName = {};
         _.each(this.collection.listFields, function(fieldName) {
+            inputType = view.options.inputType || view.inputType || {};
             mRender = undefined;
             mSearchText = undefined;
             sTitle = fieldNameToHumanReadable(fieldName);
@@ -840,6 +880,8 @@
             } else if (fieldName in view.collection.foreignFields) {
                 var foreignCollection = view.collection.foreignFields[fieldName];
                 mSearchText = function(x) { return idToName(x, foreignCollection, "humanReadableName"); };
+            } else if (inputType[fieldName] == "spinner") {
+                mRender = function(x,y,z) { return xosDataTableSpinnerTemplate( {value: x, collectionName: view.collection.collectionName, fieldName: fieldName, id: z.id} ); };
             }
             if ($.inArray(fieldName, view.collection.detailLinkFields)>=0) {
                 var collectionName = view.collection.collectionName;
@@ -850,9 +892,11 @@
             view.columnsByFieldName[fieldName] = thisColumn;
         });
 
-        deleteColumn = {sTitle: "", bSortable: false, mRender: function(x,y,z) { return xosDeleteButtonTemplate({modelName: view.collection.modelName, id: z.id}); }, mData: function() { return "delete"; }};
-        view.columnsByIndex.push(deleteColumn);
-        view.columnsByFieldName["delete"] = deleteColumn;
+        if (!view.noDeleteColumn) {
+            deleteColumn = {sTitle: "", bSortable: false, mRender: function(x,y,z) { return xosDeleteButtonTemplate({modelName: view.collection.modelName, id: z.id}); }, mData: function() { return "delete"; }};
+            view.columnsByIndex.push(deleteColumn);
+            view.columnsByFieldName["delete"] = deleteColumn;
+        };
 
         oTable = $(this.el).find("table").dataTable( {
             "bJQueryUI": true,
@@ -893,7 +937,7 @@
                 // content of the collection

                 var populateTable = function()

                 {

-                  console.log("populatetable!");

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

 

                   // clear out old row views

                   rows = [];

@@ -1044,3 +1088,25 @@
     return result;
 }
 
+choicesToOptions = function(selectedValue, choices) {
+    result="";
+    for (index in choices) {
+        choice = choices[index];
+        displayName = choice[0];
+        value = choice[1];
+        if (value == selectedValue) {
+            selected = " selected";
+        } else {
+            selected = "";
+        }
+        result = result + '<option value="' + value + '"' + selected + '>' + displayName + '</option>';
+    }
+    return result;
+}
+
+choicesToSelect = function(variable, selectedValue, choices) {
+    result = '<select name="' + variable + '" id="field_' + variable + '">' +
+             choicesToOptions(selectedValue, choices) +
+             '</select>';
+    return result;
+}
diff --git a/planetstack/core/xoslib/templates/xosAdmin.html b/planetstack/core/xoslib/templates/xosAdmin.html
index 4862cea..ac90e94 100644
--- a/planetstack/core/xoslib/templates/xosAdmin.html
+++ b/planetstack/core/xoslib/templates/xosAdmin.html
@@ -158,9 +158,14 @@
   <% args = arguments; %>

   <% _.each(detailFields, function(fieldName) { %>

      <tr><td><%= fieldNameToHumanReadable(fieldName) %>:</td>

-        <% readOnly = $.inArray(fieldName, model.readOnlyFields)>=0 ? " readonly" : "";  console.log(fieldName + " " + readOnly); console.log(model.readOnlyFields); %>

-        <% if (fieldName in foreignFields) { %>

+        <% readOnly = $.inArray(fieldName, model.readOnlyFields)>=0 ? " readonly" : "";  %>

+        <% if (fieldName in choices) { %>

+            <td><%= choicesToSelect(fieldName, model.attributes[fieldName], choices[fieldName]) %></td>

+        <% } else if (fieldName in foreignFields) { %>

             <td><%= idToSelect(fieldName, model.attributes[fieldName], foreignFields[fieldName], "humanReadableName", readOnly) %></td>

+        <% } else if (inputType[fieldName] == "spinner") { %>

+            <!-- note: I never finished working on this spinner stuff! -->

+            <td><%= xosSpinnerTemplate({id: "picker_" + fieldName, fieldName: fieldName, value: model.attributes[fieldName]}) %></td>

         <% } else if (inputType[fieldName] == "checkbox") { %>

             <td><input type="checkbox" name="<%= fieldName %>" <% if (model.attributes[fieldName]) print("checked"); %><%= readOnly %>></td>

         <% } else if (inputType[fieldName] == "picker") { %>

@@ -223,9 +228,19 @@
   </div>
 </script>
 
+<script type="text/template" id="xos-datatable-spinner-template">
+    <!-- arguments: value, id, collectionName, fieldName -->
+    <%= value %> <a href='#increase/<%= collectionName %>/<%= id %>/<%= fieldName %>'>more</a> <a href='#decrease/<%= collectionName %>/<%= id %>/<%= fieldName %>'>less</a>
+</script>
+
+<script type="text/template" id="xos-spinner-template">
+    <!-- arguments: fieldName, id, value -->
+    <input name="<%= fieldName %>" class="xos-spinner" id="<%= id %>">
+    <% detailView.viewInitializers.push( function() { init_spinner("#" + id, value); } ); %>
+</script>
 
 <script type="text/template" id="xos-picker-template">
-    <!-- arguments: unpickedItems, pickedItems -->
+    <!-- arguments: unpickedItems, pickedItems, fieldName, id -->
     <div id="<%= id %>">
     <div class="picker_row">
     <div class="picker_column">

@@ -259,6 +274,15 @@
     <% detailView.viewInitializers.push( function() { init_picker("#" + id); } ); %>
 </script>
 
+<script type="text/template" id="xos-sliceselector-option">
+   <%= name %>
+</script>
+
+<script type="text/template" id="xos-sliceselector-select">
+    <select>
+    </select>
+</script>
+
 <script>
 xosInlineDetailButtonsTemplate = _.template($("#xos-inline-detail-buttons-template").html());
 xosListHeaderTemplate = _.template($("#xos-list-header-template").html());
@@ -268,5 +292,7 @@
 xosBackendStatusIconTemplate = _.template($("#xos-backend-status-icon-template").html());
 xosBackendStatusTextTemplate = _.template($("#xos-backend-status-text-template").html());
 xosPickerTemplate = _.template($("#xos-picker-template").html());
+xosSpinnerTemplate = _.template($("#xos-spinner-template").html());
+xosDataTableSpinnerTemplate = _.template($("#xos-datatable-spinner-template").html());
 </script>