Redesigns Multiple Select Filter Widget 71/68071/3
authorParker Berberian <pberberian@iol.unh.edu>
Tue, 18 Jun 2019 18:58:27 +0000 (14:58 -0400)
committerParker Berberian <pberberian@iol.unh.edu>
Fri, 21 Jun 2019 14:56:34 +0000 (10:56 -0400)
Makes the filter widget work as it should so that it can
be integrated with the rest of the Django form handling
nicely.

Also fixes a lot of ugly code tangential to the widget.

Change-Id: Ib92db8e584f3d2162c6c43a18b75a57273bb18f5
Signed-off-by: Parker Berberian <pberberian@iol.unh.edu>
dashboard/src/booking/forms.py
dashboard/src/booking/quick_deployer.py
dashboard/src/templates/booking/quick_deploy.html
dashboard/src/templates/dashboard/multiple_select_filter_widget.html
dashboard/src/templates/resource/steps/define_hardware.html
dashboard/src/workflow/forms.py
dashboard/src/workflow/resource_bundle_workflow.py

index e48b293..df88cc6 100644 (file)
@@ -48,8 +48,7 @@ class QuickBookingForm(forms.Form):
         )
 
         attrs = FormUtils.getLabData(0)
-        attrs['selection_data'] = 'false'
-        self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(attrs=attrs))
+        self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(**attrs))
         self.fields['length'] = forms.IntegerField(
             widget=NumberInput(
                 attrs={
index ac69c8c..11f5437 100644 (file)
@@ -96,25 +96,22 @@ class BookingPermissionException(Exception):
     pass
 
 
-def parse_host_field(host_field_contents):
-    host_json = json.loads(host_field_contents)
-    lab_dict = host_json['labs'][0]
-    lab_id = list(lab_dict.keys())[0]
-    lab_user_id = int(lab_id.split("_")[-1])
-    lab = Lab.objects.get(lab_user__id=lab_user_id)
-
-    host_dict = host_json['hosts'][0]
-    profile_id = list(host_dict.keys())[0]
-    profile_id = int(profile_id.split("_")[-1])
-    profile = HostProfile.objects.get(id=profile_id)
-
-    # check validity of field data before trying to apply to models
-    if len(host_json['labs']) != 1:
+def parse_host_field(host_json):
+    lab, profile = (None, None)
+    lab_dict = host_json['lab']
+    for lab_info in lab_dict.values():
+        if lab_info['selected']:
+            lab = Lab.objects.get(lab_user__id=lab_info['id'])
+
+    host_dict = host_json['host']
+    for host_info in host_dict.values():
+        if host_info['selected']:
+            profile = HostProfile.objects.get(pk=host_info['id'])
+
+    if lab is None:
         raise NoLabSelectedError("No lab was selected")
-    if not lab:
-        raise LabDNE("Lab with provided ID does not exist")
-    if not profile:
-        raise HostProfileDNE("Host type with provided ID does not exist")
+    if profile is None:
+        raise HostProfileDNE("No Host was selected")
 
     return lab, profile
 
index 2fbd035..8cf8481 100644 (file)
 </div>
 </div>
 <script type="text/javascript">
-    var normalize = function(data)
-    {
-        //converts the top level keys in data to map to lists
-        var normalized = {}
-        for( var key in data ){
-            normalized[key] = [];
-            for( var subkey in data[key] ){
-                normalized[key].push(data[key][subkey]);
-            }
-        }
-        return normalized;
-    }
-    var update_page_contents = function(response)
-    {
-        document.open();
-        document.write(response);
-        document.close();
-    }
 
-    //form hamdler code
-    submit_form = function()
+    function submit_form()
     {
-        //altered from initial prototype: form submits automatically,
-        //but needs formatting for multiple select field
-        var data = normalize(result);
-        data = JSON.stringify(data);
-        document.getElementById("filter_field").value = data;
+        //formats data for form submission
+        document.getElementById("filter_field").value = JSON.stringify(result);
     }
 
-    var sup_image_dict = {{ image_filter|safe }};
-    var sup_installer_dict = {{ installer_filter|safe }};
-    var sup_scenario_dict = {{ scenario_filter|safe }};
-
-    function imageHider() {
-        var data = normalize(result);
-        var drop = document.getElementById("id_image");
+    function hide_dropdown(drop_id) {
+        var drop = document.getElementById(drop_id);
+        //select 'blank' option
         for( var i=0; i < drop.length; i++ )
         {
             if ( drop.options[i].text == '---------' )
-            {
                 drop.selectedIndex = i;
-            }
         }
 
+        //cross browser hide children
         $('#id_image').children().hide();
-
         for( var i = 0; i < drop.childNodes.length; i++ )
         {
             drop.childNodes[i].disabled = true; // closest we can get on safari to hiding it outright
         }
+    }
+
+    function get_selected_value(key){
+        for( var attr in result[key] ){
+            if( attr in {} )
+                continue;
+            else
+                return attr;
+        }
+        return null;
+    }
+
+    var sup_image_dict = {{ image_filter|safe }};
+    var sup_installer_dict = {{ installer_filter|safe }};
+    var sup_scenario_dict = {{ scenario_filter|safe }};
+
+    function imageHider() {
+        var drop = document.getElementById("id_image");
 
+        hide_dropdown("id_image");
 
-        var empty_map = {}
+        var lab_pk = get_selected_value("lab");
+        var host_pk = get_selected_value("host");
 
         for ( var i=0; i < drop.childNodes.length; i++ )
         {
             var image_object = sup_image_dict[drop.childNodes[i].value];
             if( image_object ) //weed out empty option
             {
-                var lab_pk = ""
-                for( var j in data["labs"][0] )
-                {
-                    if( j in {} ) { continue; }
-                    else { lab_pk = j; break; }
-                }
-                var host_pk = "";
-                for( var j in data["hosts"][0] )
-                {
-                    if( j in {} ) { continue; }
-                    else { host_pk = j; break; }
-                }
                 if( image_object.host_profile == host_pk && image_object.lab == lab_pk )
                 {
                     drop.childNodes[i].style.display = "inherit";
     document.getElementById('id_installer').addEventListener('change', scenarioHider);
 
     function dropFilter(target, target_filter, master) {
-        ob = document.getElementById(target);
+        var dropdown = document.getElementById(target);
 
-        for(var i=0; i<ob.options.length; i++) {
-            if ( ob.options[i].text == '---------' ) {
-                ob.selectedIndex = i;
-                }
-        }
+        hide_dropdown(target);
 
-        targ_id = "#" + target;
-
-        $(targ_id).children().hide();
-
-        for (var i = 0; i < document.getElementById(target).childNodes.length; i++)
-        {
-            document.getElementById(target).childNodes[i].disabled = true;
-        }
         var drop = document.getElementById(master);
         var opts = target_filter[drop.options[drop.selectedIndex].value];
         if (!opts) {
             opts = {};
         }
-        var emptyMap = {}
 
         var map = Object.create(null);
         for (var i = 0; i < opts.length; i++) {
             map[j] = true;
         }
 
-        for (var i = 0; i < document.getElementById(target).childNodes.length; i++) {
-            if (document.getElementById(target).childNodes[i].value in opts && !(document.getElementById(target).childNodes[i].value in emptyMap) ) {
-                document.getElementById(target).childNodes[i].style.display = "inherit";
-                document.getElementById(target).childNodes[i].disabled = false;
+        for (var i = 0; i < dropdown.childNodes.length; i++) {
+            if (dropdown.childNodes[i].value in opts && !(dropdown.childNodes[i].value in {}) ) {
+                dropdown.childNodes[i].style.display = "inherit";
+                dropdown.childNodes[i].disabled = false;
             }
         }
     }
 </script>
     <button id="quick_booking_confirm" onclick="submit_form();" class="btn btn-success">Confirm</button>
 </form>
-<script>
-    //context vars
-    var prefill_host_selection = "{{host_select_field_prefill_data|default:""|safe}}";
-    var prefill_purpose = "{{prefill_purpose|default:""|safe}}";
-    var prefill_project = "{{prefill_project|default:""|safe}}";
-    var prefill_hostname = "{{prefill_hostname|default:""|safe}}";
-
-    //to handle prefill
-    function prefill_host_select_field(data)
-    {
-        //
-        if(data)
-        {
-            make_selection(data);
-        }
-    }
-
-    //call init functions
-    prefill_host_select_field(prefill_host_selection);
-</script>
 {% endblock %}
index 536fdcc..3a7e148 100644 (file)
@@ -4,6 +4,7 @@
     grid-template-columns: 1fr 1fr 1fr;
     border: 0px;
 }
+
 .class_grid_wrapper {
     border: 0px;
     text-align: center;
     display: grid;
     grid-template-columns: 1fr 1fr;
 }
+
 .grid-item {
     cursor: pointer;
-    border:1px solid #cccccc;
+    border: 1px solid #cccccc;
     border-radius: 5px;
-    margin:20px;
+    margin: 20px;
     height: 200px;
     padding: 7px;
-    transition-property: box-shadow, background-color;
-    transition-duration: .2s;
-}
-
-.grid-item:hover {
-    box-shadow: 0px 0px 7px 0px rgba(0,0,0,0.45);
-    transition-property: box-shadow;
-    transition-duration: .2s;
-
+    transition: border-color ease-in-out .1s,box-shadow ease-in-out .1s;
+    box-shadow: 0 1px 1px rgba(0,0,0,.075);
 }
 
 .selected_node {
-    box-shadow: 0px 0px 4px 0px rgba(0,0,0,0.45);
-    background-color: #fff;
-    transition-property: background-color;
-    transition-duration: .2s;
+    border-color: #40c640;
+    box-shadow: 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(109, 243, 76, 0.6);
+    transition: border-color ease-in-out .1s,box-shadow ease-in-out .1s;
 }
 
 .disabled_node {
     cursor: not-allowed;
     background-color: #EFEFEF;
-    transition-property: box-shadow;
-    transition-duration: .2s;
-    border: 1px solid #ccc;
 }
 
-.disabled_node:hover {
-}
+.disabled_node:hover {}
 
 .cleared_node {
     background-color: #FFFFFF;
 }
 
-.grid-item-header
-{
+.grid-item-header {
     font-weight: bold;
     font-size: 20px;
     margin-top: 10px;
 }
 
-#dropdown_wrapper > div > h5 {
-    margin: 12px;
-    display: inline-block;
-    vertical-align: middle;
+.dropdown_item {
+    border: 1px;
+    border-style: solid;
+    border-color: lightgray;
+    border-radius: 5px;
+    margin: 20px;
+    padding: 2px;
+    grid-column: 1;
+    display: grid;
+    grid-template-columns: 1fr 3fr 1fr;
+    justify-items: center;
 }
 
-#dropdown_wrapper > div > button {
-    padding: 7px;
+.dropdown_item > button {
     margin: 2px;
-    float: right;
-    width: 80px;
+    justify-self: end;
 }
-#dropdown_wrapper > div > input {
-    padding: 7px;
-    margin: 2px;
-    float: right;
-    width: 300px;
-    width: calc(100% - 240px);
+
+.dropdown_item > h5 {
+    margin: auto;
 }
 
-#dropdown_wrapper > div {
-    border:2px;
-    border-style:none;
-    border-color:black;
-    border-radius: 5px;
-    margin:20px;
-    padding: 2px;
-    box-shadow: 0px 0px 7px 0px rgba(0,0,0,0.75);
-    transition-property: box-shadow, background-color;
-    transition-duration: .2s;
-    display: inline-block;
-    vertical-align: middle;
+.dropdown_item > input {
+    padding: 7px;
+    margin: 2px;
+    width: 90%;
 }
 
 #dropdown_wrapper {
     display: grid;
-    grid-template-columns: 3fr 5fr;
+    grid-template-columns: 4fr 5fr;
 }
-
 </style>
+
 <input name="filter_field" id="filter_field" type="hidden"/>
 <div id="grid_wrapper" class="grid_wrapper">
-{% for object_class, object_list in filter_objects %}
+{% for object_class, object_list in display_objects %}
     <div class="class_grid_wrapper">
         <div style="display:inline-block;margin:auto">
             <h4>{{object_class}}</h4>
                 <p class="grid-item-header">{{obj.name}}</p>
                 <p class="grid-item-description">{{obj.description}}</p>
                 <button type="button" class="btn btn-success grid-item-select-btn" onclick="processClick(
-                    '{{obj.id}}',
-                    {% if obj.multiple %}true
-                    {% else %}false
-                    {% endif %});">{% if obj.multiple %}Add{% else %}Select{% endif %}</button>
+                    '{{obj.id}}');">{% if obj.multiple %}Add{% else %}Select{% endif %}</button>
             </div>
-            <input type="hidden" name="{{obj.id}}_selected" value="false"/>
         {% endfor %}
         </div>
     </div>
 
 <script>
 var initialized = false;
-var mapping = {{ mapping|safe }};
+var inputs = [];
+var graph_neighbors = {{ neighbors|safe }};
 var filter_items = {{ filter_items|safe }};
 var result = {};
-var selection = {{selection_data|default_if_none:"null"|safe}};
 var dropdown_count = 0;
 
-{% if selection_data %}
-make_selection({{selection_data|safe}});
-{% endif %}
+{% if initial_value %}
 
-function make_selection( selection_data ){
-    if(!initialized) {
-        filter_field_init();
-    }
-    for(var k in selection_data) {
-        selected_items = selection_data[k];
-        for( var selected_item in selected_items ){
-            var node = filter_items[selected_item];
-            if(!node['multiple']){
-                var input_value = selected_items[selected_item];
-                if( input_value != 'false' ) {
-                    select(node);
-                    markAndSweep(node);
-                }
-                var div = document.getElementById(selected_item)
-                var inputs = div.parentNode.getElementsByTagName("input")
-                var input = div.parentNode.getElementsByTagName("input")[0]
-                input.value = input_value;
-                updateResult(selected_item);
-            } else {
-                make_multiple_selection(selected_items, selected_item);
+var initial_value = {{ initial_value|safe }};
+
+
+function make_selection( initial_data ){
+    try_init();
+    for(var item_class in initial_data) {
+        var selected_items = initial_data[item_class];
+        for( var node_id in selected_items ){
+            var node = filter_items[node_id];
+            var selection_data = selected_items[node_id]
+            if( selection_data.selected ) {
+                select(node);
+                markAndSweep(node);
+                updateResult(node);
+            }
+            if(node['multiple']){
+                make_multiple_selection(node, selection_data);
             }
         }
     }
 }
 
-function make_multiple_selection(data, item_class){
-    var node = filter_items[item_class];
-    select(node);
-    markAndSweep(node);
-    prepop_data = data[item_class];
-    for(var i=0; i<prepop_data.length; i++){
-        var div = add_item_prepopulate(node, prepop_data[i]);
-        updateObjectResult(div);
+function make_multiple_selection(node, selection_data){
+    prepop_data = selection_data.values;
+    for(var k in prepop_data){
+        var div = add_item_prepopulate(node, prepop_data[k]);
+        updateObjectResult(node, div.id, prepop_data[k]);
     }
 }
 
+make_selection({{initial_value|safe}});
+
+{% endif %}
+
 function markAndSweep(root){
-    for(var nodeId in filter_items) {
-        node = filter_items[nodeId];
+    for(var i in filter_items) {
+        node = filter_items[i];
         node['marked'] = true; //mark all nodes
-        //clears grey background of everything
     }
 
     toCheck = [root];
-
     while(toCheck.length > 0){
         node = toCheck.pop();
         if(!node['marked']) {
@@ -200,11 +172,10 @@ function markAndSweep(root){
             continue;
         }
         node['marked'] = false; //mark as visited
-        if(node['follow'] || node == root){ //add neighbors if we want to follow this node (labs)
-            var mappingId = node.id
-            var neighbors = mapping[mappingId];
-            for(var neighId in neighbors) {
-                neighId = neighbors[neighId];
+        if(node['follow'] || node == root){ //add neighbors if we want to follow this node
+            var neighbors = graph_neighbors[node.id];
+            for(var i in neighbors) {
+                var neighId = neighbors[i];
                 var neighbor = filter_items[neighId];
                 toCheck.push(neighbor);
             }
@@ -212,8 +183,8 @@ function markAndSweep(root){
     }
 
     //now remove all nodes still marked
-    for(var nodeId in filter_items){
-        node = filter_items[nodeId];
+    for(var i in filter_items){
+        node = filter_items[i];
         if(node['marked']){
             disable_node(node);
         }
@@ -224,7 +195,7 @@ function process(node) {
     if(node['selected']) {
         markAndSweep(node);
     }
-    else {
+    else {  //TODO: make this not dumb
         var selected = []
         //remember the currently selected, then reset everything and reselect one at a time
         for(var nodeId in filter_items) {
@@ -233,7 +204,6 @@ function process(node) {
                 selected.push(node);
             }
             clear(node);
-
         }
         for(var i=0; i<selected.length; i++) {
             node = selected[i];
@@ -249,9 +219,6 @@ function select(node) {
     elem.classList.remove('cleared_node');
     elem.classList.remove('disabled_node');
     elem.classList.add('selected_node');
-    var input = elem.parentNode.getElementsByTagName("input")[0];
-    input.disabled = false;
-    input.value = true;
 }
 
 function clear(node) {
@@ -261,7 +228,6 @@ function clear(node) {
     elem.classList.add('cleared_node')
     elem.classList.remove('disabled_node');
     elem.classList.remove('selected_node');
-    elem.parentNode.getElementsByTagName("input")[0].disabled = true;
 }
 
 function disable_node(node) {
@@ -271,21 +237,22 @@ function disable_node(node) {
     elem.classList.remove('cleared_node');
     elem.classList.add('disabled_node');
     elem.classList.remove('selected_node');
-    elem.parentNode.getElementsByTagName("input")[0].disabled = true;
 }
 
-function processClick(id, multiple){
-    if(!initialized){
-        filter_field_init();
-    }
-    var element = document.getElementById(id);
+function processClick(id){
+    try_init();
     var node = filter_items[id];
-    if(!node['selectable']){
+    if(!node['selectable'])
         return;
+
+    if(node['multiple']){
+        return processClickMultiple(node);
+    } else {
+        return processClickSingle(node);
     }
-    if(multiple){
-        return processClickMultipleObject(node);
-    }
+}
+
+function processClickSingle(node){
     node['selected'] = !node['selected']; //toggle on click
 
     if(node['selected']) {
@@ -294,21 +261,20 @@ function processClick(id, multiple){
         clear(node);
     }
     process(node);
-    updateResult(id);
+    updateResult(node);
 }
 
-function processClickMultipleObject(node){
+function processClickMultiple(node){
     select(node);
-    add_node(node);
+    var div = add_node(node);
     process(node);
+    updateObjectResult(node, div.id, "");
 }
 
 function add_node(node){
-    return add_item_prepopulate(node, {});
+    return add_item_prepopulate(node, false);
 }
 
-inputs = []
-
 function restrictchars(input){
     if( input.validity.patternMismatch ){
         input.setCustomValidity("Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed");
@@ -318,8 +284,7 @@ function restrictchars(input){
     checkunique(input);
 }
 
-function checkunique(tocheck)
-{
+function checkunique(tocheck){
     val = tocheck.value;
     for( var i = 0; i < inputs.length; i++ )
     {
@@ -333,109 +298,83 @@ function checkunique(tocheck)
     tocheck.setCustomValidity("");
 }
 
-function add_item_prepopulate(node, prepopulate){
-    inputs = [];
-    var div = document.createElement("DIV");
-    div.class = node['id'];
-    div.id = "dropdown_" + dropdown_count;
-    dropdown_count++;
-    var label = document.createElement("H5");
-    label.appendChild(document.createTextNode(node['name']));
-    div.appendChild(label);
-    button = document.createElement("BUTTON");
+function make_remove_button(div, node){
+    var button = document.createElement("BUTTON");
     button.type = "button";
     button.appendChild(document.createTextNode("Remove"));
     button.classList.add("btn-danger");
     button.classList.add("btn");
-    div.appendChild(button);
-    for(var i=0; i<node['forms'].length; i++){
-        form = node['forms'][i];
-        var input = document.createElement("INPUT");
-        input.type = form['type'];
-        input.name = form['name'];
-        input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})";
-        input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"
-        input.placeholder = form['placeholder'];
-        inputs.push(input);
-        input.onchange = function() { updateObjectResult(div); restrictchars(this); };
-        input.oninput = function() { restrictchars(this); };
-        if(form['name'] in prepopulate){
-            input.value = prepopulate[form['name']];
-        }
-        div.appendChild(input);
-    }
-    //add class id to dropdown object
-    var hiddenInput = document.createElement("INPUT");
-    hiddenInput.type = "hidden";
-    hiddenInput.name = "class";
-    hiddenInput.value = node['id'];
-    div.appendChild(hiddenInput);
     button.onclick = function(){
-        remove_dropdown(div.id);
+        remove_dropdown(div.id, node.id);
     }
+    return button;
+}
+
+function make_input(div, node, prepopulate){
+    var input = document.createElement("INPUT");
+    input.type = node.form.type;
+    input.name = node.id + node.form.name
+    input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})";
+    input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"
+    input.placeholder = node.form.placeholder;
+    inputs.push(input);
+    input.onchange = function() { updateObjectResult(node, div.id, input.value); restrictchars(this); };
+    input.oninput = function() { restrictchars(this); };
+    if(prepopulate)
+        input.value = prepopulate;
+    return input;
+}
+
+function add_item_prepopulate(node, prepopulate){
+    var div = document.createElement("DIV");
+    div.id = "dropdown_" + dropdown_count;
+    div.classList.add("dropdown_item");
+    dropdown_count++;
+    var label = document.createElement("H5")
+    label.appendChild(document.createTextNode(node['name']))
+    div.appendChild(label);
+    div.appendChild(make_input(div, node, prepopulate));
+    div.appendChild(make_remove_button(div, node));
     document.getElementById("dropdown_wrapper").appendChild(div);
-    var linebreak = document.createElement("BR");
-    document.getElementById("dropdown_wrapper").appendChild(linebreak);
-    updateObjectResult(div);
     return div;
 }
 
-function remove_dropdown(id){
-    var div = document.getElementById(id);
+function remove_dropdown(div_id, node_id){
+    var div = document.getElementById(div_id);
+    var node = filter_items[node_id]
     var parent = div.parentNode;
     div.parentNode.removeChild(div);
+    delete result[node.class][node.id]['values'][div.id];
+
     //checks if we have removed last item in class
-    var deselect_class = true;
-    var div_inputs = div.getElementsByTagName("input");
-    var div_class = div_inputs[div_inputs.length-1].value;
-    var result_class = document.getElementById(div_class).parentNode.parentNode.id;
-    delete result[result_class][div.id];
-    for(var i=0; i<parent.children.length; i++){
-        var inputs = parent.children[i].getElementsByTagName("input");
-        var object_class = "";
-        for(var k=0; k<inputs.length; k++){
-            if(inputs[k].name == "class"){
-                object_class = inputs[k].value;
-            }
-        }
-        if(object_class == div_class){
-            deselect_class = false;
-        }
-    }
-    if(deselect_class){
-        clear(filter_items[div_class]);
+    if(jQuery.isEmptyObject(result[node.class][node.id]['values'])){
+        delete result[node.class][node.id];
+        clear(node);
     }
 }
-
-function updateResult(nodeId){
-    if(!initialized){
-        filter_field_init();
-    }
-    if(!filter_items[nodeId]['multiple']){
-        var node = document.getElementById(nodeId);
-        var value = {}
-        value[nodeId] = node.parentNode.getElementsByTagName("input")[0].value;
-        result[node.parentNode.id] = {};
-        result[node.parentNode.id][nodeId] = value;
+function updateResult(node){
+    try_init();
+    if(!node['multiple']){
+        result[node.class][node.id] = {selected: node.selected, id: node.model_id}
+        if(!node.selected)
+            delete result[node.class][node.id];
     }
 }
 
-function updateObjectResult(parentElem){
-    node_type = document.getElementById(parentElem.class).parentNode.id;
-    input = {};
-    inputs = parentElem.getElementsByTagName("input");
-    for(var i in inputs){
-        var e = inputs[i];
-        input[e.name] = e.value;
-    }
-    result[node_type][parentElem.id] = input;
+function updateObjectResult(node, childKey, childValue){
+    try_init();
+    if(!result[node.class][node.id])
+        result[node.class][node.id] = {selected: true, id: node.model_id, values: {}}
+
+    result[node.class][node.id]['values'][childKey] = childValue;
 }
 
-function filter_field_init() {
+function try_init() {
+    if(initialized) return;
     for(nodeId in filter_items) {
-        element = document.getElementById(nodeId);
-        node = filter_items[nodeId];
-        result[element.parentNode.id] = {}
+        var element = document.getElementById(nodeId);
+        var node = filter_items[nodeId];
+        result[node.class] = {}
         }
     initialized = true;
 }
index 933b4ab..9192842 100644 (file)
@@ -26,7 +26,7 @@ var normalize = function(data){
     }
     return normalized;
 }
-var data = normalize(result);
+var data = result;
 data = JSON.stringify(data);
 document.getElementById("filter_field").value = data;
 var formData = $("#define_hardware_form").serialize();
index 0fb45d6..ee44ecd 100644 (file)
@@ -242,124 +242,101 @@ class BookingMetaForm(forms.Form):
 
 
 class MultipleSelectFilterWidget(forms.Widget):
-    def __init__(self, attrs=None):
-        super(MultipleSelectFilterWidget, self).__init__(attrs)
-        self.attrs = attrs
+    def __init__(self, *args, display_objects=None, filter_items=None, neighbors=None, **kwargs):
+        super(MultipleSelectFilterWidget, self).__init__(*args, **kwargs)
+        self.display_objects = display_objects
+        self.filter_items = filter_items
+        self.neighbors = neighbors
         self.template_name = "dashboard/multiple_select_filter_widget.html"
 
     def render(self, name, value, attrs=None, renderer=None):
-        attrs = self.attrs
-        self.context = self.get_context(name, value, attrs)
-        html = render_to_string(self.template_name, context=self.context)
+        context = self.get_context(name, value, attrs)
+        html = render_to_string(self.template_name, context=context)
         return mark_safe(html)
 
     def get_context(self, name, value, attrs):
-        return attrs
+        return {
+            'display_objects': self.display_objects,
+            'neighbors': self.neighbors,
+            'filter_items': self.filter_items,
+            'initial_value': value
+        }
 
 
 class MultipleSelectFilterField(forms.Field):
 
-    def __init__(self, required=True, widget=None, label=None, initial=None,
-                 help_text='', error_messages=None, show_hidden_initial=False,
-                 validators=(), localize=False, disabled=False, label_suffix=None):
-        """from the documentation:
-        # required -- Boolean that specifies whether the field is required.
-        #             True by default.
-        # widget -- A Widget class, or instance of a Widget class, that should
-        #           be used for this Field when displaying it. Each Field has a
-        #           default Widget that it'll use if you don't specify this. In
-        #           most cases, the default widget is TextInput.
-        # label -- A verbose name for this field, for use in displaying this
-        #          field in a form. By default, Django will use a "pretty"
-        #          version of the form field name, if the Field is part of a
-        #          Form.
-        # initial -- A value to use in this Field's initial display. This value
-        #            is *not* used as a fallback if data isn't given.
-        # help_text -- An optional string to use as "help; text" for this Field.
-        # error_messages -- An optional dictionary to override the default
-        #                   messages that the field will raise.
-        # show_hidden_initial -- Boolean that specifies if it is needed to render a
-        #                        hidden widget with initial value after widget.
-        # validators -- List of additional validators to use
-        # localize -- Boolean that specifies if the field should be localized.
-        # disabled -- Boolean that specifies whether the field is disabled, that
-        #             is its widget is shown in the form but not editable.
-        # label_suffix -- Suffix to be added to the label. Overrides
-        #                 form's label_suffix.
-        """
-        # this is bad, but django forms are annoying
-        self.widget = widget
-        if self.widget is None:
-            self.widget = MultipleSelectFilterWidget()
-        super(MultipleSelectFilterField, self).__init__(
-            required=required,
-            widget=self.widget,
-            label=label,
-            initial=None,
-            help_text=help_text,
-            error_messages=error_messages,
-            show_hidden_initial=show_hidden_initial,
-            validators=validators,
-            localize=localize,
-            disabled=disabled,
-            label_suffix=label_suffix
-        )
+    def __init__(self, **kwargs):
+        self.initial = kwargs.get("initial")
+        super().__init__(**kwargs)
 
-        def clean(data):
-            """
-            This method will raise a django.forms.ValidationError or return clean data
-            """
-            return data
+    def to_python(self, value):
+        return json.loads(value)
 
 
 class FormUtils:
     @staticmethod
-    def getLabData(multiple_selectable_hosts):
+    def getLabData(multiple_hosts=False):
         """
         Gets all labs and thier host profiles and returns a serialized version the form can understand.
         Should be rewritten with a related query to make it faster
-        Should be moved outside of global scope
         """
+        # javascript truthy variables
+        true = 1
+        false = 0
+        if multiple_hosts:
+            multiple_hosts = true
+        else:
+            multiple_hosts = false
         labs = {}
         hosts = {}
         items = {}
-        mapping = {}
+        neighbors = {}
         for lab in Lab.objects.all():
-            slab = {}
-            slab['id'] = "lab_" + str(lab.lab_user.id)
-            slab['name'] = lab.name
-            slab['description'] = lab.description
-            slab['selected'] = 0
-            slab['selectable'] = 1
-            slab['follow'] = 1
-            if not multiple_selectable_hosts:
-                slab['follow'] = 0
-            slab['multiple'] = 0
-            items[slab['id']] = slab
-            mapping[slab['id']] = []
-            labs[slab['id']] = slab
+            lab_node = {
+                'id': "lab_" + str(lab.lab_user.id),
+                'model_id': lab.lab_user.id,
+                'name': lab.name,
+                'description': lab.description,
+                'selected': false,
+                'selectable': true,
+                'follow': false,
+                'multiple': false,
+                'class': 'lab'
+            }
+            if multiple_hosts:
+                # "follow" this lab node to discover more hosts if allowed
+                lab_node['follow'] = true
+            items[lab_node['id']] = lab_node
+            neighbors[lab_node['id']] = []
+            labs[lab_node['id']] = lab_node
+
             for host in lab.hostprofiles.all():
-                shost = {}
-                shost['forms'] = [{"name": "host_name", "type": "text", "placeholder": "hostname"}]
-                shost['id'] = "host_" + str(host.id)
-                shost['name'] = host.name
-                shost['description'] = host.description
-                shost['selected'] = 0
-                shost['selectable'] = 1
-                shost['follow'] = 0
-                shost['multiple'] = multiple_selectable_hosts
-                items[shost['id']] = shost
-                mapping[slab['id']].append(shost['id'])
-                if shost['id'] not in mapping:
-                    mapping[shost['id']] = []
-                mapping[shost['id']].append(slab['id'])
-                hosts[shost['id']] = shost
-
-        filter_objects = [("labs", labs.values()), ("hosts", hosts.values())]
+                host_node = {
+                    'form': {"name": "host_name", "type": "text", "placeholder": "hostname"},
+                    'id': "host_" + str(host.id),
+                    'model_id': host.id,
+                    'name': host.name,
+                    'description': host.description,
+                    'selected': false,
+                    'selectable': true,
+                    'follow': false,
+                    'multiple': multiple_hosts,
+                    'class': 'host'
+                }
+                if multiple_hosts:
+                    host_node['values'] = []  # place to store multiple values
+                items[host_node['id']] = host_node
+                neighbors[lab_node['id']].append(host_node['id'])
+                if host_node['id'] not in neighbors:
+                    neighbors[host_node['id']] = []
+                neighbors[host_node['id']].append(lab_node['id'])
+                hosts[host_node['id']] = host_node
+
+        display_objects = [("lab", labs.values()), ("host", hosts.values())]
 
         context = {
-            'filter_objects': filter_objects,
-            'mapping': mapping,
+            'display_objects': display_objects,
+            'neighbors': neighbors,
             'filter_items': items
         }
         return context
@@ -368,14 +345,10 @@ class FormUtils:
 class HardwareDefinitionForm(forms.Form):
 
     def __init__(self, *args, **kwargs):
-        selection_data = kwargs.pop("selection_data", False)
         super(HardwareDefinitionForm, self).__init__(*args, **kwargs)
-        attrs = FormUtils.getLabData(1)
-        attrs['selection_data'] = selection_data
+        attrs = FormUtils.getLabData(multiple_hosts=True)
         self.fields['filter_field'] = MultipleSelectFilterField(
-            widget=MultipleSelectFilterWidget(
-                attrs=attrs
-            )
+            widget=MultipleSelectFilterWidget(**attrs)
         )
 
 
index ced355f..a4657ab 100644 (file)
@@ -52,65 +52,47 @@ class Define_Hardware(WorkflowStep):
     description = "Choose the type and amount of machines you want"
     short_title = "hosts"
 
+    def __init__(self, *args, **kwargs):
+        self.form = None
+        super().__init__(*args, **kwargs)
+
     def get_context(self):
         context = super(Define_Hardware, self).get_context()
-        selection_data = {"hosts": {}, "labs": {}}
-        models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
-        hosts = models.get("hosts", [])
-        for host in hosts:
-            profile_id = "host_" + str(host.profile.id)
-            if profile_id not in selection_data['hosts']:
-                selection_data['hosts'][profile_id] = []
-            selection_data['hosts'][profile_id].append({"host_name": host.resource.name, "class": profile_id})
-
-        if models.get("bundle", GenericResourceBundle()).lab:
-            selection_data['labs'] = {"lab_" + str(models.get("bundle").lab.lab_user.id): "true"}
-
-        form = HardwareDefinitionForm(
-            selection_data=selection_data
-        )
-        context['form'] = form
+        context['form'] = self.form or HardwareDefinitionForm()
         return context
 
-    def render(self, request):
-        self.context = self.get_context()
-        return render(request, self.template, self.context)
-
     def update_models(self, data):
-        data = json.loads(data['filter_field'])
+        data = data['filter_field']
         models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
         models['hosts'] = []  # This will always clear existing data when this step changes
         models['interfaces'] = {}
         if "bundle" not in models:
             models['bundle'] = GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER))
-        host_data = data['hosts']
+        host_data = data['host']
         names = {}
-        for host_dict in host_data:
-            id = host_dict['class']
-            # bit of formatting
-            id = int(id.split("_")[-1])
+        for host_profile_dict in host_data.values():
+            id = host_profile_dict['id']
             profile = HostProfile.objects.get(id=id)
             # instantiate genericHost and store in repo
-            name = host_dict['host_name']
-            if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})", name):
-                raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
-            if name in names:
-                raise NonUniqueHostnameException("All hosts must have unique names")
-            names[name] = True
-            genericResource = GenericResource(bundle=models['bundle'], name=name)
-            genericHost = GenericHost(profile=profile, resource=genericResource)
-            models['hosts'].append(genericHost)
-            for interface_profile in profile.interfaceprofile.all():
-                genericInterface = GenericInterface(profile=interface_profile, host=genericHost)
-                if genericHost.resource.name not in models['interfaces']:
-                    models['interfaces'][genericHost.resource.name] = []
-                models['interfaces'][genericHost.resource.name].append(genericInterface)
+            for name in host_profile_dict['values'].values():
+                if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})", name):
+                    raise InvalidHostnameException("Invalid hostname: '" + name + "'")
+                if name in names:
+                    raise NonUniqueHostnameException("All hosts must have unique names")
+                names[name] = True
+                genericResource = GenericResource(bundle=models['bundle'], name=name)
+                genericHost = GenericHost(profile=profile, resource=genericResource)
+                models['hosts'].append(genericHost)
+                for interface_profile in profile.interfaceprofile.all():
+                    genericInterface = GenericInterface(profile=interface_profile, host=genericHost)
+                    if genericHost.resource.name not in models['interfaces']:
+                        models['interfaces'][genericHost.resource.name] = []
+                    models['interfaces'][genericHost.resource.name].append(genericInterface)
 
         # add selected lab to models
-        for lab_dict in data['labs']:
-            if list(lab_dict.values())[0]:  # True for lab the user selected
-                lab_user_id = int(list(lab_dict.keys())[0].split("_")[-1])
-                models['bundle'].lab = Lab.objects.get(lab_user__id=lab_user_id)
+        for lab_dict in data['lab'].values():
+            if lab_dict['selected']:
+                models['bundle'].lab = Lab.objects.get(lab_user__id=lab_dict['id'])
                 break  # if somehow we get two 'true' labs, we only use one
 
         # return to repo
@@ -133,15 +115,11 @@ class Define_Hardware(WorkflowStep):
         try:
             self.form = HardwareDefinitionForm(request.POST)
             if self.form.is_valid():
-                if len(json.loads(self.form.cleaned_data['filter_field'])['labs']) != 1:
-                    self.set_invalid("Please select one lab")
-                else:
-                    self.update_models(self.form.cleaned_data)
-                    self.update_confirmation()
-                    self.set_valid("Step Completed")
+                self.update_models(self.form.cleaned_data)
+                self.update_confirmation()
+                self.set_valid("Step Completed")
             else:
                 self.set_invalid("Please complete the fields highlighted in red to continue")
-                pass
         except Exception as e:
             self.set_invalid(str(e))
         self.context = self.get_context()