Merge branch 'master' of ssh://git.planet-lab.org/git/plstackapi
diff --git a/planetstack/core/xoslib/dashboards/xosAdminDashboard.html b/planetstack/core/xoslib/dashboards/xosAdminDashboard.html
index af9a0f4..bf10240 100644
--- a/planetstack/core/xoslib/dashboards/xosAdminDashboard.html
+++ b/planetstack/core/xoslib/dashboards/xosAdminDashboard.html
@@ -14,6 +14,7 @@
 <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/picker.js"></script>
 <script src="{{ STATIC_URL }}/js/xosAdminSite.js"></script>
 
 <script type="text/template" id="xos-log-template">
diff --git a/planetstack/core/xoslib/static/css/xosAdminSite.css b/planetstack/core/xoslib/static/css/xosAdminSite.css
index ad9eb47..91d4928 100644
--- a/planetstack/core/xoslib/static/css/xosAdminSite.css
+++ b/planetstack/core/xoslib/static/css/xosAdminSite.css
@@ -107,3 +107,11 @@
 #xos-confirm-dialog {
     display: none;
 }
+
+.picker_row {
+  display: table;
+}
+.picker_column {
+  display: table-cell;
+  padding: 10px;
+}
diff --git a/planetstack/core/xoslib/static/js/picker.js b/planetstack/core/xoslib/static/js/picker.js
new file mode 100644
index 0000000..0302cf4
--- /dev/null
+++ b/planetstack/core/xoslib/static/js/picker.js
@@ -0,0 +1,50 @@
+function init_picker(selector, ordered) {
+    //console.log("init_picker");
+    //console.log($(selector));
+
+    var addBtn = $(selector).find(".btn-picker-add");
+    var removeBtn = $(selector).find(".btn-picker-remove");
+    var upBtn = $(selector).find(".btn-picker-up");
+    var downBtn = $(selector).find(".btn-picker-down");
+    var from = $(selector).find(".select-picker-from");
+    var to = $(selector).find(".select-picker-to");
+
+    if (!ordered) {
+        upBtn.hide();
+        downBtn.hide();
+    }
+
+    addBtn.click(function(){
+        console.log("add");
+        from.find(":selected").each( function() {
+            to.append("<option value='"+$(this).val()+"'>"+$(this).text()+"</option>");

+            $(this).remove();

+        });

+    });

+    removeBtn.click(function(){

+        console.log("remove");

+        to.find(":selected").each( function() {

+            from.append("<option value='"+$(this).val()+"'>"+$(this).text()+"</option>");

+            $(this).remove();

+        });

+    });

+    upBtn.bind('click', function() {

+        to.find(":selected").each( function() {

+            var newPos = to.find('option').index(this) - 1;

+            if (newPos > -1) {

+                to.find("option").eq(newPos).before("<option value='"+$(this).val()+"' selected='selected'>"+$(this).text()+"</option>");

+                $(this).remove();

+            }

+        });

+    });

+    downBtn.bind('click', function() {

+        var countOptions = to.find("option").size();

+        to.find(":selected").each( function() {

+            var newPos = to.find("option").index(this) + 1;

+            if (newPos < countOptions) {

+                to.find("option").eq(newPos).after("<option value='"+$(this).val()+"' selected='selected'>"+$(this).text()+"</option>");

+                $(this).remove();

+            }

+        });

+    });

+};

diff --git a/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js b/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js
index 554ff5f..b0ee0dc 100644
--- a/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js
+++ b/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js
@@ -79,6 +79,20 @@
                 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.
@@ -324,6 +338,7 @@
 
         attrs.inputType = attrs.inputType || {};
         attrs.foreignFields = attrs.foreignFields || {};
+        attrs.m2mFields = attrs.m2mFields || {};
         attrs.readOnlyFields = attrs.readOnlyFields || [];
         attrs.detailLinkFields = attrs.detailLinkFields || ["id","name"];
 
@@ -473,9 +488,11 @@
 
         define_model(this, { urlRoot: DEPLOYMENT_API,
                              relatedCollections: {"nodes": "deployment", "slivers": "deploymentNetwork", "networkDeployments": "deployment", "userDeployments": "deployment"},
+                             m2mFields: {"flavors": "flavors", "sites": "sites", "images": "images"},
                              modelName: "deployment",
                              listFields: ["backend_status", "id", "name", "backend_type", "admin_tenant"],
-                             detailFields: ["backend_status", "name", "backend_type", "admin_tenant"],
+                             detailFields: ["backend_status", "name", "backend_type", "admin_tenant", "flavors", "sites", "images"],
+                             inputType: {"flavors": "picker", "sites": "picker", "images": "picker"},
                              });
 
         define_model(this, {urlRoot: IMAGE_API,
@@ -529,9 +546,10 @@
 
         define_model(this, {urlRoot: FLAVOR_API,
                             modelName: "flavor",
+                            m2mFields: {"deployments": "deployments"},
                             listFields: ["backend_status", "id", "name", "flavor", "order", "default"],
-                            detailFields: ["backend_status", "name", "description", "flavor", "order", "default"],
-                            inputType: {"default": "checkbox"},
+                            detailFields: ["backend_status", "name", "description", "flavor", "order", "default", "deployments"],
+                            inputType: {"default": "checkbox", "deployments": "picker"},
                             });
 
         // enhanced REST
diff --git a/planetstack/core/xoslib/static/js/xoslib/xos-util.js b/planetstack/core/xoslib/static/js/xoslib/xos-util.js
index 638ba9a..3fd597b 100644
--- a/planetstack/core/xoslib/static/js/xoslib/xos-util.js
+++ b/planetstack/core/xoslib/static/js/xoslib/xos-util.js
@@ -1,5 +1,15 @@
 // misc utility functions
 
+function idInArray(id, arr) {
+    // because sometimes ids are strings and sometimes they're integers
+    for (index in arr) {
+        if (id.toString() == arr[index].toString()) {
+            return true;
+        }
+    }
+    return false;
+}
+
 function assert(outcome, description) {
     if (!outcome) {
         console.log(description);
diff --git a/planetstack/core/xoslib/static/js/xoslib/xosHelper.js b/planetstack/core/xoslib/static/js/xoslib/xosHelper.js
index 65ccdbd..fe975aa 100644
--- a/planetstack/core/xoslib/static/js/xoslib/xosHelper.js
+++ b/planetstack/core/xoslib/static/js/xoslib/xosHelper.js
@@ -404,6 +404,8 @@
 XOSDetailView = Marionette.ItemView.extend({
             tagName: "div",
 
+            viewInitializers: [],
+
             events: {"click button.btn-xos-save-continue": "submitContinueClicked",
                      "click button.btn-xos-save-leave": "submitLeaveClicked",
                      "click button.btn-xos-save-another": "submitAddAnotherClicked",
@@ -420,6 +422,12 @@
                 this.synchronous = false;
             },
 
+            onShow: function() {
+                _.each(this.viewInitializers, function(initializer) {
+                    initializer();
+                });
+            },
+
             afterSave: function(e) {
             },
 
@@ -574,7 +582,6 @@
             onFormDataInvalid: function(errors) {
                 var self=this;
                 var markErrors = function(value, key) {
-                    console.log("name='" + key + "'");
                     var $inputElement = self.$el.find("[name='" + key + "']");
                     var $inputContainer = $inputElement.parent();
                     //$inputContainer.find(".help-inline").remove();
@@ -593,6 +600,7 @@
                                                     detailLinkFields: this.model.detailLinkFields,
                                                     inputType: this.model.inputType,
                                                     model: this.model,
+                                                    detailView: this,
                                          }},
 });
 
@@ -972,6 +980,10 @@
     return xos.idToName(id, collectionName, fieldName);
 };
 
+makeIdToName = function(collectionName, fieldName) {
+    return function(id) { return idToName(id, collectionName, fieldName); }
+};
+
 /* Constructs lists of <option> html blocks for items in a collection.
 
    selectedId = the id of an object that should be selected, if any
diff --git a/planetstack/core/xoslib/templates/xosAdmin.html b/planetstack/core/xoslib/templates/xosAdmin.html
index acc318b..92b5046 100644
--- a/planetstack/core/xoslib/templates/xosAdmin.html
+++ b/planetstack/core/xoslib/templates/xosAdmin.html
@@ -159,6 +159,9 @@
             <td><%= idToSelect(fieldName, model.attributes[fieldName], foreignFields[fieldName], "humanReadableName", readOnly) %></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") { %>

+            <% lookupFunc = makeIdToName(model.m2mFields[fieldName], "humanReadableName"); %>

+            <td><%= xosPickerTemplate({pickedItems: model.attributes[fieldName], unpickedItems: model.getChoices(fieldName,true), id: "picker_" + fieldName, detailView: detailView, lookupFunc: lookupFunc}) %></td>

         <% } else if (fieldName=="backend_status") { %>

             <td><%= xosBackendStatusTextTemplate.apply(this, args) %></td>

         <% } else { %>

@@ -216,6 +219,42 @@
   </div>
 </script>
 
+
+<script type="text/template" id="xos-picker-template">
+    <!-- arguments: unpickedItems, pickedItems -->
+    <div id="<%= id %>">
+    <div class="picker_row">
+    <div class="picker_column">

+    <div>Available</div>

+    <select name="pickerfrom" class="select-picker-from" multiple size="5">

+        <% _.each(unpickedItems, function(item) { %>

+           <option value="<%= item %>"><%= lookupFunc? lookupFunc(item) : item %></option>
+        <% });%>

+    </select>

+    </div>

+    <div class="picker_column">

+    <br>

+    <div class="btn btn-success btn-picker-add">Add &raquo;</div><br><br>

+    <div class="btn btn-success btn-picker-remove">&laquo; Remove</div>

+    </div>

+    <div class="picker_column">

+    <div>Selected</div>

+    <select name="pickerto" class="select-picker-to" multiple size="5">

+        <% _.each(pickedItems, function(item) { %>

+           <option value="<%= item %>"><%= lookupFunc ? lookupFunc(item) : item %></option>
+        <% }); %>

+    </select>

+    </div>

+    <div class="picker_column">

+    <br>

+    <div class="btn btn-success btn-picker-up">Up</div><br><br>

+    <div class="btn btn-success btn-picker-down">Down</div>

+    </div>

+    </div>
+    </div>
+    <% detailView.viewInitializers.push( function() { init_picker("#" + id); } ); %>
+</script>
+
 <script>
 xosInlineDetailButtonsTemplate = _.template($("#xos-inline-detail-buttons-template").html());
 xosListHeaderTemplate = _.template($("#xos-list-header-template").html());
@@ -224,5 +263,6 @@
 xosDetailLinkTemplate = _.template($("#xos-detail-link-template").html());
 xosBackendStatusIconTemplate = _.template($("#xos-backend-status-icon-template").html());
 xosBackendStatusTextTemplate = _.template($("#xos-backend-status-text-template").html());
+xosPickerTemplate = _.template($("#xos-picker-template").html());
 </script>