Refactor selector step logic 14/67914/10
authorSawyer Bergeron <sbergeron@iol.unh.edu>
Thu, 23 May 2019 17:30:37 +0000 (13:30 -0400)
committerSawyer Bergeron <sbergeron@iol.unh.edu>
Fri, 31 May 2019 19:57:34 +0000 (15:57 -0400)
Change-Id: I61e361e63da7453b2eee0e0c162a6f4e48460128
Signed-off-by: Sawyer Bergeron <sbergeron@iol.unh.edu>
dashboard/src/booking/forms.py
dashboard/src/booking/lib.py [new file with mode: 0644]
dashboard/src/booking/quick_deployer.py
dashboard/src/booking/views.py
dashboard/src/templates/dashboard/genericselect.html [new file with mode: 0644]
dashboard/src/templates/dashboard/searchable_select_multiple.html
dashboard/src/workflow/booking_workflow.py
dashboard/src/workflow/forms.py
dashboard/src/workflow/models.py
dashboard/src/workflow/sw_bundle_workflow.py
dashboard/src/workflow/urls.py

index de427ab..e48b293 100644 (file)
@@ -10,12 +10,13 @@ import django.forms as forms
 from django.forms.widgets import NumberInput
 
 from workflow.forms import (
-    SearchableSelectMultipleWidget,
     MultipleSelectFilterField,
     MultipleSelectFilterWidget,
     FormUtils)
 from account.models import UserProfile
 from resource_inventory.models import Image, Installer, Scenario
+from workflow.forms import SearchableSelectMultipleField
+from booking.lib import get_user_items, get_user_field_opts
 
 
 class QuickBookingForm(forms.Form):
@@ -27,16 +28,11 @@ class QuickBookingForm(forms.Form):
     scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=False)
 
     def __init__(self, data=None, user=None, *args, **kwargs):
-        chosen_users = []
         if "default_user" in kwargs:
             default_user = kwargs.pop("default_user")
         else:
             default_user = "you"
         self.default_user = default_user
-        if "chosen_users" in kwargs:
-            chosen_users = kwargs.pop("chosen_users")
-        elif data and "users" in data:
-            chosen_users = data.getlist("users")
 
         super(QuickBookingForm, self).__init__(data=data, **kwargs)
 
@@ -44,12 +40,13 @@ class QuickBookingForm(forms.Form):
             Image.objects.filter(public=True) | Image.objects.filter(owner=user)
         )
 
-        self.fields['users'] = forms.CharField(
-            widget=SearchableSelectMultipleWidget(
-                attrs=self.build_search_widget_attrs(chosen_users, default_user=default_user)
-            ),
-            required=False
+        self.fields['users'] = SearchableSelectMultipleField(
+            queryset=UserProfile.objects.select_related('user').exclude(user=user),
+            items=get_user_items(exclude=user),
+            required=False,
+            **get_user_field_opts()
         )
+
         attrs = FormUtils.getLabData(0)
         attrs['selection_data'] = 'false'
         self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(attrs=attrs))
diff --git a/dashboard/src/booking/lib.py b/dashboard/src/booking/lib.py
new file mode 100644 (file)
index 0000000..8132c75
--- /dev/null
@@ -0,0 +1,36 @@
+##############################################################################
+# Copyright (c) 2019 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+from account.models import UserProfile
+
+
+def get_user_field_opts():
+    return {
+        'show_from_noentry': False,
+        'show_x_results': 5,
+        'results_scrollable': True,
+        'selectable_limit': -1,
+        'placeholder': 'Search for other users',
+        'name': 'users',
+        'disabled': False
+    }
+
+
+def get_user_items(exclude=None):
+    qs = UserProfile.objects.select_related('user').exclude(user=exclude)
+    items = {}
+    for up in qs:
+        item = {
+            'id': up.id,
+            'expanded_name': up.full_name,
+            'small_name': up.user.username,
+            'string': up.email_addr
+        }
+        items[up.id] = item
+    return items
index 763c8a0..ac69c8c 100644 (file)
@@ -12,7 +12,6 @@ import json
 import uuid
 import re
 from django.db.models import Q
-from django.contrib.auth.models import User
 from datetime import timedelta
 from django.utils import timezone
 from account.models import Lab
@@ -321,12 +320,8 @@ def create_from_form(form, request):
     )
     booking.pdf = PDFTemplater.makePDF(booking)
 
-    users_field = users_field[2:-2]
-    if users_field:  # may be empty after split, if no collaborators entered
-        users_field = json.loads(users_field)
-        for collaborator in users_field:
-            user = User.objects.get(id=collaborator['id'])
-            booking.collaborators.add(user)
+    for collaborator in users_field:  # list of UserProfiles
+        booking.collaborators.add(collaborator.user)
 
     booking.save()
 
index 8211a0c..13e9d01 100644 (file)
@@ -47,7 +47,7 @@ def quick_create(request):
 
         context['lab_profile_map'] = profiles
 
-        context['form'] = QuickBookingForm(initial={}, chosen_users=[], default_user=request.user.username, user=request.user)
+        context['form'] = QuickBookingForm(default_user=request.user.username, user=request.user)
 
         context.update(drop_filter(request.user))
 
diff --git a/dashboard/src/templates/dashboard/genericselect.html b/dashboard/src/templates/dashboard/genericselect.html
new file mode 100644 (file)
index 0000000..fc29ee6
--- /dev/null
@@ -0,0 +1,71 @@
+{% extends "workflow/viewport-element.html" %}
+{% load staticfiles %}
+
+{% load bootstrap3 %}
+
+{% block content %}
+
+<style>
+#{{select_type}}_form_div {
+        width: 100%;
+        padding: 5%;
+    }
+
+    .panel {
+        border: none;
+    }
+    .select_panels {
+        width: 100%;
+        display: grid;
+        grid-template-columns: 45% 10% 45%;
+
+    }
+
+    .panel_center {
+        text-align: center;
+        border: none;
+
+    }
+    .panel_center p{
+        font-size: 20pt;
+    }
+</style>
+
+<div id="{{select_type}}_form_div">
+    <div class="select_panels">
+        <div class="panel_chooser panel">
+            <form id="{{select_type}}_select_form" method="post" action="" class="form" id="{{select_type}}selectorform">
+            {% csrf_token %}
+            {{ form|default:"<p>no form loaded</p>" }}
+            {% buttons %}
+
+            {% endbuttons %}
+            </form>
+        </div>
+        <div class="panel_center panel"><p>OR</p></div>
+        <div class="panel_add panel">
+            <button class="btn {% if disabled %} disabled {% endif %}"
+                style="width: 100%; height: 100%;"
+                {% if not disabled %}onclick="parent.add_wf({{addable_type_num}})"
+                {% endif %}>Create {{select_type_title}}
+            </button>
+        </div>
+    </div>
+</div>
+<script>
+    {% if disabled %}
+    disable();
+    {% endif %}
+</script>
+
+{% endblock content %}
+{% block onleave %}
+var form = $("#{{select_type}}_select_form");
+var formData = form.serialize();
+var req = new XMLHttpRequest();
+req.open("POST", "/wf/workflow/", false);
+req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+req.onerror = function() { alert("problem with form submission"); }
+req.send(formData);
+{% endblock %}
+
index c08fbe5..69394ab 100644 (file)
@@ -7,9 +7,10 @@
         <p>Please make a different selection, as the current config conflicts with the selected pod</p>
         {% endif %}
     </div>
-    <input id="user_field" name="ignore_this" class="form-control" autocomplete="off" type="text" placeholder="{{placeholder}}" value="{{initial.name}}" oninput="search(this.value)"
+    <input id="user_field" name="ignore_this" class="form-control" autocomplete="off" type="text" placeholder="{{placeholder}}" value="" oninput="search(this.value)"
     {% if disabled %} disabled {% endif %}
     >
+    </input>
 
     <input type="hidden" id="selector" name="{{ name }}" class="form-control" style="display: none;"
     {% if disabled %} disabled {% endif %}
 
     <ul id="drop_results"></ul>
 
-
-    <div id="default_entry_wrap" style="display: none;">
-        <div class="list_entry unremovable_list_entry">
-            <p id="default_text" class="full_name"></p>
-            <button class="btn-remove btn disabled">remove</button>
-        </div>
-    </div>
-
     <div id="added_list">
 
     </div>
 
 <script type="text/javascript">
     //flags
-    var show_from_noentry = {{show_from_noentry|default:"false"}};
-    var show_x_results = {{show_x_results|default:-1}};
-    var results_scrollable = {{results_scrollable|default:"false"}};
-    var selectable_limit = {{selectable_limit|default:-1}};
-    var field_name  = "{{name|default:"users"}}";
-    var placeholder = "{{placeholder|default:"begin typing"}}";
-    var default_entry = "{{default_entry}}";
+    var show_from_noentry = {{show_from_noentry|yesno:"true,false"}}; // whether to show any results before user starts typing
+    var show_x_results = {{show_x_results|default:-1}}; // how many results to show at a time, -1 shows all results
+    var results_scrollable = {{results_scrollable|yesno:"true,false"}}; // whether list should be scrollable
+    var selectable_limit = {{selectable_limit|default:-1}}; // how many selections can be made, -1 allows infinitely many
+    var placeholder = "{{placeholder|default:"begin typing"}}"; // placeholder that goes in text box
 
     //needed info
-    var items = {{items|safe}}
+    var items = {{items|safe}} // items to add to trie. Type is a dictionary of dictionaries with structure:
+        /*
+        {
+            id# : {
+                "id": any, identifiable on backend
+                "small_name": string, displayed first (before separator), searchable (use for e.g. username)
+                "expanded_name": string, displayed second (after separator), searchable (use for e.g. email address)
+                "string": string, not displayed, still searchable
+            }
+        }
+        */
+
+    /* used later:
+    {{ selectable_limit }}: changes what number displays for field
+    {{ name }}: form identifiable name, relevant for backend
+        // when submitted, form will contain field data in post with name as the key
+    {{ placeholder }}: "greyed out" contents put into search field initially to guide user as to what they're searching for
+    {{ initial }}: in search_field_init(), marked safe, an array of id's each referring to an id from items
+    */
 
     //tries
     var expanded_name_trie = {}
     string_trie.isComplete = false;
 
     var added_items = [];
-    var initial_log = {{ initial|safe }};
-
-    var added_template = {{ added_list|default:"{}" }};
-
-    if( default_entry )
-    {
-        var default_entry_div = document.getElementById("default_entry_wrap");
-        default_entry_div.style.display = "inherit";
-
-        var entry_p = document.getElementById("default_text");
-        entry_p.innerText = default_entry;
-    }
 
     search_field_init();
 
     function select_item(item_id)
     {
         //TODO make faster
-        var item = items[item_id];
+        var item = items[item_id]['id'];
         if( (selectable_limit > -1 && added_items.length < selectable_limit) || selectable_limit < 0 )
         {
             if( added_items.indexOf(item) == -1 )
         document.getElementById("user_field").focus();
     }
 
-    function edit_item(item_id){
-        var wf_type = "{{wf_type}}";
-        parent.add_edit_wf(wf_type, item_id);
-    }
-
     function update_selected_list()
     {
         document.getElementById("added_number").innerText = added_items.length;
 
         for( var key in added_items )
         {
-            item = added_items[key];
+            item_id = added_items[key];
+            item = items[item_id];
 
             list_html += '<div class="list_entry"><p class="full_name">'
                 + item["expanded_name"]
                 + '</p><button onclick="remove_item('
                 + Object.values(items).indexOf(item)
                 + ')" class="btn-remove btn">remove</button>';
-            {% if edit %}
-                list_html += '<button onclick="edit_item('
-                + item['id']
-                + ')" class="btn-remove btn">edit</button>';
-            {% endif %}
                 list_html += '</div>';
         }
 
index eb87728..d8c8646 100644 (file)
 ##############################################################################
 
 from django.contrib import messages
-from django.shortcuts import render
-from django.contrib.auth.models import User
 from django.utils import timezone
 
-import json
 from datetime import timedelta
 
 from booking.models import Booking
-from workflow.models import WorkflowStep
+from workflow.models import WorkflowStep, AbstractSelectOrCreate
 from workflow.forms import ResourceSelectorForm, SWConfigSelectorForm, BookingMetaForm
-from resource_inventory.models import GenericResourceBundle, ResourceBundle, ConfigBundle
+from resource_inventory.models import GenericResourceBundle, ConfigBundle
 
 
-class Resource_Select(WorkflowStep):
-    template = 'booking/steps/resource_select.html'
+"""
+subclassing notes:
+    subclasses have to define the following class attributes:
+        self.repo_key: main output of step, where the selected/created single selector
+            result is placed at the end
+        self.confirm_key:
+"""
+
+
+class Abstract_Resource_Select(AbstractSelectOrCreate):
+    form = ResourceSelectorForm
+    template = 'dashboard/genericselect.html'
     title = "Select Resource"
     description = "Select a resource template to use for your deployment"
     short_title = "pod select"
 
     def __init__(self, *args, **kwargs):
-        super(Resource_Select, self).__init__(*args, **kwargs)
-        self.repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE
-        self.repo_check_key = False
-        self.confirm_key = "booking"
-
-    def get_context(self):
-        context = super(Resource_Select, self).get_context()
-        default = []
+        super().__init__(*args, **kwargs)
+        self.select_repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE
+        self.confirm_key = self.workflow_type
 
-        chosen_bundle = self.repo_get(self.repo_key, False)
-        if chosen_bundle:
-            default.append(chosen_bundle.id)
+    def alert_bundle_missing(self):
+        self.set_invalid("Please select a valid resource bundle")
 
-        bundle = chosen_bundle
-        edit = self.repo_get(self.repo.EDIT, False)
+    def get_form_queryset(self):
         user = self.repo_get(self.repo.SESSION_USER)
-        context['form'] = ResourceSelectorForm(
-            data={"user": user},
-            chosen_resource=default,
-            bundle=bundle,
-            edit=edit
-        )
-        return context
+        qs = GenericResourceBundle.objects.filter(owner=user)
+        return qs
 
-    def post_render(self, request):
-        form = ResourceSelectorForm(request.POST)
-        context = self.get_context()
-        if form.is_valid():
-            data = form.cleaned_data['generic_resource_bundle']
-            data = data[2:-2]
-            if not data:
-                self.set_invalid("Please select a valid bundle")
-                return render(request, self.template, context)
-            selected_bundle = json.loads(data)
-            if len(selected_bundle) < 1:
-                self.set_invalid("Please select a valid bundle")
-                return render(request, self.template, context)
-            selected_id = selected_bundle[0]['id']
-            gresource_bundle = None
-            try:
-                selected_id = int(selected_id)
-                gresource_bundle = GenericResourceBundle.objects.get(id=selected_id)
-            except ValueError:
-                # we want the bundle in the repo
-                gresource_bundle = self.repo_get(
-                    self.repo.GRESOURCE_BUNDLE_MODELS,
-                    {}
-                ).get("bundle", GenericResourceBundle())
-            self.repo_put(
-                self.repo_key,
-                gresource_bundle
-            )
-            confirm = self.repo_get(self.repo.CONFIRMATION)
-            if self.confirm_key not in confirm:
-                confirm[self.confirm_key] = {}
-            confirm[self.confirm_key]["resource name"] = gresource_bundle.name
-            self.repo_put(self.repo.CONFIRMATION, confirm)
-            messages.add_message(request, messages.SUCCESS, 'Form Validated Successfully', fail_silently=True)
-            self.set_valid("Step Completed")
-            return render(request, self.template, context)
-        else:
-            messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True)
-            self.set_invalid("Please complete the fields highlighted in red to continue")
-            return render(request, self.template, context)
+    def get_page_context(self):
+        return {
+            'select_type': 'resource',
+            'select_type_title': 'Resource Bundle',
+            'addable_type_num': 1
+        }
 
+    def put_confirm_info(self, bundle):
+        confirm_dict = self.repo_get(self.repo.CONFIRMATION)
+        if self.confirm_key not in confirm_dict:
+            confirm_dict[self.confirm_key] = {}
+        confirm_dict[self.confirm_key]["Resource Template"] = bundle.name
+        self.repo_put(self.repo.CONFIRMATION, confirm_dict)
 
-class Booking_Resource_Select(Resource_Select):
 
-    def __init__(self, *args, **kwargs):
-        super(Booking_Resource_Select, self).__init__(*args, **kwargs)
-        self.repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE
-        self.confirm_key = "booking"
+class Booking_Resource_Select(Abstract_Resource_Select):
+    workflow_type = "booking"
 
-    def get_context(self):
-        context = super(Booking_Resource_Select, self).get_context()
-        return context
 
-    def post_render(self, request):
-        response = super(Booking_Resource_Select, self).post_render(request)
-        models = self.repo_get(self.repo.BOOKING_MODELS, {})
-        if "booking" not in models:
-            models['booking'] = Booking()
-        booking = models['booking']
-        resource = self.repo_get(self.repo_key, False)
-        if resource:
-            try:
-                booking.resource.template = resource
-            except Exception:
-                booking.resource = ResourceBundle(template=resource)
-        models['booking'] = booking
-        self.repo_put(self.repo.BOOKING_MODELS, models)
-        return response
-
-
-class SWConfig_Select(WorkflowStep):
-    template = 'booking/steps/swconfig_select.html'
+class SWConfig_Select(AbstractSelectOrCreate):
     title = "Select Software Configuration"
     description = "Choose the software and related configurations you want to have used for your deployment"
     short_title = "pod config"
+    form = SWConfigSelectorForm
 
-    def post_render(self, request):
-        form = SWConfigSelectorForm(request.POST)
-        if form.is_valid():
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.select_repo_key = self.repo.SELECTED_CONFIG_BUNDLE
+        self.confirm_key = "booking"
 
-            bundle_json = form.cleaned_data['software_bundle']
-            bundle_json = bundle_json[2:-2]  # Stupid django string bug
-            if not bundle_json:
-                self.set_invalid("Please select a valid config")
-                return self.render(request)
-            bundle_json = json.loads(bundle_json)
-            if len(bundle_json) < 1:
-                self.set_invalid("Please select a valid config")
-                return self.render(request)
-            bundle = None
-            id = int(bundle_json[0]['id'])
-            bundle = ConfigBundle.objects.get(id=id)
-
-            grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE)
-
-            if grb and bundle.bundle != grb:
-                self.set_invalid("Incompatible config selected for resource bundle")
-                return self.render(request)
-            if not grb:
-                self.repo_set(self.repo.SELECTED_GRESOURCE_BUNDLE, bundle.bundle)
+    def alert_bundle_missing(self):
+        self.set_invalid("Please select a valid pod config")
 
-            models = self.repo_get(self.repo.BOOKING_MODELS, {})
-            if "booking" not in models:
-                models['booking'] = Booking()
-            models['booking'].config_bundle = bundle
-            self.repo_put(self.repo.BOOKING_MODELS, models)
-            confirm = self.repo_get(self.repo.CONFIRMATION)
-            if "booking" not in confirm:
-                confirm['booking'] = {}
-            confirm['booking']["configuration name"] = bundle.name
-            self.repo_put(self.repo.CONFIRMATION, confirm)
-            self.set_valid("Step Completed")
-            messages.add_message(request, messages.SUCCESS, 'Form Validated Successfully', fail_silently=True)
-        else:
-            self.set_invalid("Please select or create a valid config")
-            messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True)
+    def get_form_queryset(self):
+        user = self.repo_get(self.repo.SESSION_USER)
+        grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE)
+        qs = ConfigBundle.objects.filter(owner=user).filter(bundle=grb)
+        return qs
 
-        return self.render(request)
+    def put_confirm_info(self, bundle):
+        confirm_dict = self.repo_get(self.repo.CONFIRMATION)
+        if self.confirm_key not in confirm_dict:
+            confirm_dict[self.confirm_key] = {}
+        confirm_dict[self.confirm_key]["Software Configuration"] = bundle.name
+        self.repo_put(self.repo.CONFIRMATION, confirm_dict)
 
-    def get_context(self):
-        context = super(SWConfig_Select, self).get_context()
-        default = []
-        bundle = None
-        chosen_bundle = None
-        created_bundle = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE)
-        booking = self.repo_get(self.repo.BOOKING_MODELS, {}).get("booking", False)
-        try:
-            chosen_bundle = booking.config_bundle
-            default.append(chosen_bundle.id)
-            bundle = chosen_bundle
-        except Exception:
-            if created_bundle:
-                default.append(created_bundle.id)
-                bundle = created_bundle
-        edit = self.repo_get(self.repo.EDIT, False)
-        grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE)
-        context['form'] = SWConfigSelectorForm(chosen_software=default, bundle=bundle, edit=edit, resource=grb)
-        return context
+    def get_page_context(self):
+        return {
+            'select_type': 'swconfig',
+            'select_type_title': 'Software Config',
+            'addable_type_num': 2
+        }
 
 
 class Booking_Meta(WorkflowStep):
@@ -214,23 +123,17 @@ class Booking_Meta(WorkflowStep):
                 initial['info_file'] = info
             users = models.get("collaborators", [])
             for user in users:
-                default.append(user.id)
+                default.append(user.userprofile)
         except Exception:
             pass
 
-        default_user = self.repo_get(self.repo.SESSION_USER)
-        if default_user is None:
-            # TODO: error
-            default_user = "you"
-        else:
-            default_user = default_user.username
+        owner = self.repo_get(self.repo.SESSION_USER)
 
-        context['form'] = BookingMetaForm(initial=initial, chosen_users=default, default_user=default_user)
+        context['form'] = BookingMetaForm(initial=initial, user_initial=default, owner=owner)
         return context
 
     def post_render(self, request):
-        form = BookingMetaForm(data=request.POST)
-        context = self.get_context()
+        form = BookingMetaForm(data=request.POST, owner=request.user)
 
         forms = self.repo_get(self.repo.BOOKING_FORMS, {})
 
@@ -253,15 +156,11 @@ class Booking_Meta(WorkflowStep):
             for key in ['length', 'project', 'purpose']:
                 confirm['booking'][key] = form.cleaned_data[key]
 
-            user_data = form.cleaned_data['users']
+            userprofile_list = form.cleaned_data['users']
             confirm['booking']['collaborators'] = []
-            user_data = user_data[2:-2]  # fixes malformed string from querydict
-            if user_data:
-                form_users = json.loads(user_data)
-                for user_json in form_users:
-                    user = User.objects.get(pk=user_json['id'])
-                    models['collaborators'].append(user)
-                    confirm['booking']['collaborators'].append(user.username)
+            for userprofile in userprofile_list:
+                models['collaborators'].append(userprofile.user)
+                confirm['booking']['collaborators'].append(userprofile.user.username)
 
             info_file = form.cleaned_data.get("info_file", False)
             if info_file:
@@ -274,5 +173,4 @@ class Booking_Meta(WorkflowStep):
         else:
             messages.add_message(request, messages.ERROR, "Form didn't validate", fail_silently=True)
             self.set_invalid("Please complete the fields highlighted in red to continue")
-            context['form'] = form  # TODO: store this form
-        return render(request, self.template, context)
+        return self.render(request)
index 6d26b5c..ea484da 100644 (file)
@@ -9,40 +9,37 @@
 
 
 import django.forms as forms
-from django.forms import widgets
+from django.forms import widgets, ValidationError
 from django.utils.safestring import mark_safe
 from django.template.loader import render_to_string
 from django.forms.widgets import NumberInput
 
+import json
+
 from account.models import Lab
 from account.models import UserProfile
 from resource_inventory.models import (
-    GenericResourceBundle,
-    ConfigBundle,
     OPNFVRole,
     Installer,
     Scenario,
 )
+from booking.lib import get_user_items, get_user_field_opts
 
 
 class SearchableSelectMultipleWidget(widgets.SelectMultiple):
     template_name = 'dashboard/searchable_select_multiple.html'
 
     def __init__(self, attrs=None):
-        self.items = attrs['set']
+        self.items = attrs['items']
         self.show_from_noentry = attrs['show_from_noentry']
         self.show_x_results = attrs['show_x_results']
-        self.results_scrollable = attrs['scrollable']
+        self.results_scrollable = attrs['results_scrollable']
         self.selectable_limit = attrs['selectable_limit']
         self.placeholder = attrs['placeholder']
         self.name = attrs['name']
-        self.initial = attrs.get("initial", "")
-        self.default_entry = attrs.get("default_entry", "")
-        self.edit = attrs.get("edit", False)
-        self.wf_type = attrs.get("wf_type")
-        self.incompatible = attrs.get("incompatible", "false")
+        self.initial = attrs.get("initial", [])
 
-        super(SearchableSelectMultipleWidget, self).__init__(attrs)
+        super(SearchableSelectMultipleWidget, self).__init__()
 
     def render(self, name, value, attrs=None, renderer=None):
 
@@ -59,132 +56,145 @@ class SearchableSelectMultipleWidget(widgets.SelectMultiple):
             'selectable_limit': self.selectable_limit,
             'placeholder': self.placeholder,
             'initial': self.initial,
-            'default_entry': self.default_entry,
-            'edit': self.edit,
-            'wf_type': self.wf_type,
-            'incompatible': self.incompatible
         }
 
 
-class ResourceSelectorForm(forms.Form):
+class SearchableSelectMultipleField(forms.Field):
+    def __init__(self, *args, required=True, widget=None, label=None, disabled=False,
+                 items=None, queryset=None, show_from_noentry=True, show_x_results=-1,
+                 results_scrollable=False, selectable_limit=-1, placeholder="search here",
+                 name="searchable_select", initial=[], **kwargs):
+        """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.
+        """
 
-    def __init__(self, data=None, **kwargs):
-        chosen_resource = ""
-        bundle = None
-        edit = False
-        if "chosen_resource" in kwargs:
-            chosen_resource = kwargs.pop("chosen_resource")
-        if "bundle" in kwargs:
-            bundle = kwargs.pop("bundle")
-        if "edit" in kwargs:
-            edit = kwargs.pop("edit")
-        super(ResourceSelectorForm, self).__init__(data=data, **kwargs)
-        queryset = GenericResourceBundle.objects.select_related("owner").all()
-        if data and 'user' in data:
-            queryset = queryset.filter(owner=data['user'])
+        self.widget = widget
+        if self.widget is None:
+            self.widget = SearchableSelectMultipleWidget(
+                attrs={
+                    'items': items,
+                    'initial': [obj.id for obj in initial],
+                    'show_from_noentry': show_from_noentry,
+                    'show_x_results': show_x_results,
+                    'results_scrollable': results_scrollable,
+                    'selectable_limit': selectable_limit,
+                    'placeholder': placeholder,
+                    'name': name,
+                    'disabled': disabled
+                }
+            )
+        self.disabled = disabled
+        self.queryset = queryset
+        self.selectable_limit = selectable_limit
 
-        attrs = self.build_search_widget_attrs(chosen_resource, bundle, edit, queryset)
+        super().__init__(disabled=disabled, **kwargs)
 
-        self.fields['generic_resource_bundle'] = forms.CharField(
-            widget=SearchableSelectMultipleWidget(attrs=attrs)
-        )
+        self.required = required
 
-    def build_search_widget_attrs(self, chosen_resource, bundle, edit, queryset):
-        resources = {}
-        for res in queryset:
-            displayable = {}
-            displayable['small_name'] = res.name
-            if res.owner:
-                displayable['expanded_name'] = res.owner.username
+    def clean(self, data):
+        data = data[0]
+        if not data:
+            if self.required:
+                raise ValidationError("Nothing was selected")
             else:
-                displayable['expanded_name'] = ""
-            displayable['string'] = res.description
-            displayable['id'] = res.id
-            resources[res.id] = displayable
-
-        attrs = {
-            'set': resources,
-            'show_from_noentry': "true",
+                return []
+        data_as_list = json.loads(data)
+        if self.selectable_limit != -1:
+            if len(data_as_list) > self.selectable_limit:
+                raise ValidationError("Too many items were selected")
+
+        items = []
+        for elem in data_as_list:
+            items.append(self.queryset.get(id=elem))
+
+        return items
+
+
+class SearchableSelectAbstractForm(forms.Form):
+    def __init__(self, *args, queryset=None, initial=[], **kwargs):
+        self.queryset = queryset
+        items = self.generate_items(self.queryset)
+        options = self.generate_options()
+
+        super(SearchableSelectAbstractForm, self).__init__(*args, **kwargs)
+        self.fields['searchable_select'] = SearchableSelectMultipleField(
+            initial=initial,
+            items=items,
+            queryset=self.queryset,
+            **options
+        )
+
+    def get_validated_bundle(self):
+        bundles = self.cleaned_data['searchable_select']
+        if len(bundles) < 1:  # don't need to check for >1, as field does that for us
+            raise ValidationError("No bundle was selected")
+        return bundles[0]
+
+    def generate_items(self, queryset):
+        raise Exception("SearchableSelectAbstractForm does not implement concrete generate_items()")
+
+    def generate_options(self, disabled=False):
+        return {
+            'show_from_noentry': True,
             'show_x_results': -1,
-            'scrollable': "true",
+            'results_scrollable': True,
             'selectable_limit': 1,
-            'name': "generic_resource_bundle",
-            'placeholder': "resource",
-            'initial': chosen_resource,
-            'edit': edit,
-            'wf_type': 1
+            'placeholder': 'Search for a Bundle',
+            'name': 'searchable_select',
+            'disabled': False
         }
-        return attrs
 
 
-class SWConfigSelectorForm(forms.Form):
+class SWConfigSelectorForm(SearchableSelectAbstractForm):
+    def generate_items(self, queryset):
+        items = {}
 
-    def __init__(self, *args, **kwargs):
-        chosen_software = ""
-        bundle = None
-        edit = False
-        resource = None
-        user = None
-        if "chosen_software" in kwargs:
-            chosen_software = kwargs.pop("chosen_software")
-
-        if "bundle" in kwargs:
-            bundle = kwargs.pop("bundle")
-        if "edit" in kwargs:
-            edit = kwargs.pop("edit")
-        if "resource" in kwargs:
-            resource = kwargs.pop("resource")
-        if "user" in kwargs:
-            user = kwargs.pop("user")
-        super(SWConfigSelectorForm, self).__init__(*args, **kwargs)
-        attrs = self.build_search_widget_attrs(chosen_software, bundle, edit, resource, user)
-        self.fields['software_bundle'] = forms.CharField(
-            widget=SearchableSelectMultipleWidget(attrs=attrs)
-        )
+        for bundle in queryset:
+            item = {}
+            item['small_name'] = bundle.name
+            item['expanded_name'] = bundle.owner.username
+            item['string'] = bundle.description
+            item['id'] = bundle.id
+            items[bundle.id] = item
 
-    def build_search_widget_attrs(self, chosen, bundle, edit, resource, user):
-        configs = {}
-        queryset = ConfigBundle.objects.select_related('owner').all()
-        if resource:
-            if user is None:
-                user = resource.owner
-            queryset = queryset.filter(bundle=resource)
-
-        if user:
-            queryset = queryset.filter(owner=user)
-
-        for config in queryset:
-            displayable = {}
-            displayable['small_name'] = config.name
-            displayable['expanded_name'] = config.owner.username
-            displayable['string'] = config.description
-            displayable['id'] = config.id
-            configs[config.id] = displayable
-
-        incompatible_choice = "false"
-        if bundle and bundle.id not in configs:
-            displayable = {}
-            displayable['small_name'] = bundle.name
-            displayable['expanded_name'] = bundle.owner.username
-            displayable['string'] = bundle.description
-            displayable['id'] = bundle.id
-            configs[bundle.id] = displayable
-            incompatible_choice = "true"
-
-        attrs = {
-            'set': configs,
-            'show_from_noentry': "true",
-            'show_x_results': -1,
-            'scrollable': "true",
-            'selectable_limit': 1,
-            'name': "software_bundle",
-            'placeholder': "config",
-            'initial': chosen,
-            'edit': edit,
-            'wf_type': 2,
-            'incompatible': incompatible_choice
-        }
-        return attrs
+        return items
+
+
+class ResourceSelectorForm(SearchableSelectAbstractForm):
+    def generate_items(self, queryset):
+        items = {}
+
+        for bundle in queryset:
+            item = {}
+            item['small_name'] = bundle.name
+            item['expanded_name'] = bundle.owner.username
+            item['string'] = bundle.description
+            item['id'] = bundle.id
+            items[bundle.id] = item
+
+        return items
 
 
 class BookingMetaForm(forms.Form):
@@ -203,65 +213,16 @@ class BookingMetaForm(forms.Form):
     project = forms.CharField(max_length=400)
     info_file = forms.CharField(max_length=1000, required=False)
 
-    def __init__(self, data=None, *args, **kwargs):
-        chosen_users = []
-        if "default_user" in kwargs:
-            default_user = kwargs.pop("default_user")
-        else:
-            default_user = "you"
-        self.default_user = default_user
-        if "chosen_users" in kwargs:
-            chosen_users = kwargs.pop("chosen_users")
-        elif data and "users" in data:
-            chosen_users = data.getlist("users")
-        else:
-            pass
-
-        super(BookingMetaForm, self).__init__(data=data, **kwargs)
-
-        self.fields['users'] = forms.CharField(
-            widget=SearchableSelectMultipleWidget(
-                attrs=self.build_search_widget_attrs(chosen_users, default_user=default_user)
-            ),
-            required=False
-        )
-
-    def build_user_list(self):
-        """
-        returns a mapping of UserProfile ids to displayable objects expected by
-        searchable multiple select widget
-        """
-        try:
-            users = {}
-            d_qset = UserProfile.objects.select_related('user').all().exclude(user__username=self.default_user)
-            for userprofile in d_qset:
-                user = {
-                    'id': userprofile.user.id,
-                    'expanded_name': userprofile.full_name,
-                    'small_name': userprofile.user.username,
-                    'string': userprofile.email_addr
-                }
+    def __init__(self, *args, user_initial=[], owner=None, **kwargs):
+        super(BookingMetaForm, self).__init__(**kwargs)
 
-                users[userprofile.user.id] = user
-
-            return users
-        except Exception:
-            pass
-
-    def build_search_widget_attrs(self, chosen_users, default_user="you"):
-
-        attrs = {
-            'set': self.build_user_list(),
-            'show_from_noentry': "false",
-            'show_x_results': 10,
-            'scrollable': "false",
-            'selectable_limit': -1,
-            'name': "users",
-            'placeholder': "username",
-            'initial': chosen_users,
-            'edit': False
-        }
-        return attrs
+        self.fields['users'] = SearchableSelectMultipleField(
+            queryset=UserProfile.objects.select_related('user').exclude(user=owner),
+            initial=user_initial,
+            items=get_user_items(exclude=owner),
+            required=False,
+            **get_user_field_opts()
+        )
 
 
 class MultipleSelectFilterWidget(forms.Widget):
@@ -298,7 +259,7 @@ class MultipleSelectFilterField(forms.Field):
         #          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.
+        # 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
index 25d7e84..be81706 100644 (file)
@@ -240,6 +240,67 @@ class WorkflowStep(object):
         return self.repo.put(key, value, self.id)
 
 
+"""
+subclassing notes:
+    subclasses have to define the following class attributes:
+        self.select_repo_key: where the selected "object" or "bundle" is to be placed in the repo
+        self.form: the form to be used
+        alert_bundle_missing(): what message to display if a user does not select/selects an invalid object
+        get_form_queryset(): generate a queryset to be used to filter available items for the field
+        get_page_context(): return simple context such as page header and other info
+"""
+
+
+class AbstractSelectOrCreate(WorkflowStep):
+    template = 'dashboard/genericselect.html'
+    title = "Select a Bundle"
+    short_title = "select"
+    description = "Generic bundle selector step"
+
+    select_repo_key = None
+    form = None  # subclasses are expected to use a form that is a subclass of SearchableSelectGenericForm
+
+    def alert_bundle_missing(self):  # override in subclasses to change message if field isn't filled out
+        self.set_invalid("Please select a valid bundle")
+
+    def post_render(self, request):
+        context = self.get_context()
+        form = self.form(request.POST, queryset=self.get_form_queryset())
+        if form.is_valid():
+            bundle = form.get_validated_bundle()
+            if not bundle:
+                self.alert_bundle_missing()
+                return render(request, self.template, context)
+            self.repo_put(self.select_repo_key, bundle)
+            self.put_confirm_info(bundle)
+            self.set_valid("Step Completed")
+        else:
+            self.alert_bundle_missing()
+            messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True)
+
+        return self.render(request)
+
+    def get_context(self):
+        default = []
+
+        bundle = self.repo_get(self.select_repo_key, False)
+        if bundle:
+            default.append(bundle)
+
+        form = self.form(queryset=self.get_form_queryset(), initial=default)
+
+        context = {'form': form, **self.get_page_context()}
+        context.update(super().get_context())
+
+        return context
+
+    def get_page_context():
+        return {
+            'select_type': 'generic',
+            'select_type_title': 'Generic Bundle'
+        }
+
+
 class Confirmation_Step(WorkflowStep):
     template = 'workflow/confirm.html'
     title = "Confirm Changes"
@@ -335,6 +396,7 @@ class Repository():
             self.el[key] = value
 
     def get(self, key, default, id):
+
         self.add_get_history(key, id)
         return self.el.get(key, default)
 
@@ -359,6 +421,7 @@ class Repository():
             errors = self.make_snapshot()
             if errors:
                 return errors
+
         # if GRB WF, create it
         if self.GRESOURCE_BUNDLE_MODELS in self.el:
             errors = self.make_generic_resource_bundle()
@@ -499,7 +562,7 @@ class Repository():
         models = self.el[self.CONFIG_MODELS]
         if 'bundle' in models:
             bundle = models['bundle']
-            bundle.bundle = bundle.bundle
+            bundle.bundle = self.el[self.SELECTED_GRESOURCE_BUNDLE]
             try:
                 bundle.save()
             except Exception as e:
@@ -537,15 +600,22 @@ class Repository():
         models = self.el[self.BOOKING_MODELS]
         owner = self.el[self.SESSION_USER]
 
+        if 'booking' in models:
+            booking = models['booking']
+        else:
+            return "BOOK, no booking model exists. CODE:0x000f"
+
+        selected_grb = None
+
         if self.SELECTED_GRESOURCE_BUNDLE in self.el:
             selected_grb = self.el[self.SELECTED_GRESOURCE_BUNDLE]
         else:
             return "BOOK, no selected resource. CODE:0x000e"
 
-        if 'booking' in models:
-            booking = models['booking']
-        else:
-            return "BOOK, no booking model exists. CODE:0x000f"
+        if self.SELECTED_CONFIG_BUNDLE not in self.el:
+            return "BOOK, no selected config bundle. CODE:0x001f"
+
+        booking.config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE]
 
         if not booking.start:
             return "BOOK, booking has no start. CODE:0x0010"
@@ -567,7 +637,6 @@ class Repository():
 
         booking.resource = resource_bundle
         booking.owner = owner
-        booking.config_bundle = booking.config_bundle
         booking.lab = selected_grb.lab
 
         is_allowed = BookingAuthManager().booking_allowed(booking, self)
index 329b716..0c558fc 100644 (file)
@@ -12,25 +12,12 @@ from django.forms import formset_factory
 
 from workflow.models import WorkflowStep
 from workflow.forms import BasicMetaForm, HostSoftwareDefinitionForm
-from workflow.booking_workflow import Resource_Select
+from workflow.booking_workflow import Abstract_Resource_Select
 from resource_inventory.models import Image, GenericHost, ConfigBundle, HostConfiguration
 
 
-# resource selection step is reused from Booking workflow
-class SWConf_Resource_Select(Resource_Select):
-    def __init__(self, *args, **kwargs):
-        super(SWConf_Resource_Select, self).__init__(*args, **kwargs)
-        self.repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE
-        self.confirm_key = "configuration"
-
-    def post_render(self, request):
-        response = super(SWConf_Resource_Select, self).post_render(request)
-        models = self.repo_get(self.repo.CONFIG_MODELS, {})
-        bundle = models.get("bundle", ConfigBundle(owner=self.repo_get(self.repo.SESSION_USER)))
-        bundle.bundle = self.repo_get(self.repo_key)  # super put grb here
-        models['bundle'] = bundle
-        self.repo_put(self.repo.CONFIG_MODELS, models)
-        return response
+class SWConf_Resource_Select(Abstract_Resource_Select):
+    workflow_type = "configuration"
 
 
 class Define_Software(WorkflowStep):
index b131d84..5a97904 100644 (file)
@@ -14,7 +14,7 @@ from django.conf import settings
 from workflow.views import step_view, delete_session, manager_view, viewport_view
 from workflow.models import Repository
 from workflow.resource_bundle_workflow import Define_Hardware, Define_Nets, Resource_Meta_Info
-from workflow.booking_workflow import SWConfig_Select, Resource_Select, Booking_Meta
+from workflow.booking_workflow import SWConfig_Select, Booking_Resource_Select, Booking_Meta
 
 app_name = 'workflow'
 urlpatterns = [
@@ -31,4 +31,4 @@ if settings.TESTING:
     urlpatterns.append(url(r'^workflow/step/resource_meta$', Resource_Meta_Info("", Repository()).test_render))
     urlpatterns.append(url(r'^workflow/step/booking_meta$', Booking_Meta("", Repository()).test_render))
     urlpatterns.append(url(r'^workflow/step/software_select$', SWConfig_Select("", Repository()).test_render))
-    urlpatterns.append(url(r'^workflow/step/resource_select$', Resource_Select("", Repository()).test_render))
+    urlpatterns.append(url(r'^workflow/step/resource_select$', Booking_Resource_Select("", Repository()).test_render))