Laas Dashboard Front End Improvements
[laas.git] / src / workflow / forms.py
1 ##############################################################################
2 # Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others.
3 # Copyright (c) 2020 Sawyer Bergeron, Sean Smith, and others.
4 #
5 # All rights reserved. This program and the accompanying materials
6 # are made available under the terms of the Apache License, Version 2.0
7 # which accompanies this distribution, and is available at
8 # http://www.apache.org/licenses/LICENSE-2.0
9 ##############################################################################
10
11
12 import django.forms as forms
13 from django.forms import widgets, ValidationError
14 from django.utils.safestring import mark_safe
15 from django.template.loader import render_to_string
16 from django.forms.widgets import NumberInput
17
18 import json
19 import urllib
20
21 from account.models import Lab
22 from account.models import UserProfile
23 from resource_inventory.models import (
24     OPNFVRole,
25     Installer,
26     Scenario
27 )
28 from resource_inventory.resource_manager import ResourceManager
29 from booking.lib import get_user_items, get_user_field_opts
30
31
32 class SearchableSelectMultipleWidget(widgets.SelectMultiple):
33     template_name = 'dashboard/searchable_select_multiple.html'
34
35     def __init__(self, attrs=None):
36         self.items = attrs['items']
37         self.show_from_noentry = attrs['show_from_noentry']
38         self.show_x_results = attrs['show_x_results']
39         self.results_scrollable = attrs['results_scrollable']
40         self.selectable_limit = attrs['selectable_limit']
41         self.placeholder = attrs['placeholder']
42         self.name = attrs['name']
43         self.initial = attrs.get("initial", [])
44
45         super(SearchableSelectMultipleWidget, self).__init__()
46
47     def render(self, name, value, attrs=None, renderer=None):
48
49         context = self.get_context(attrs)
50         return mark_safe(render_to_string(self.template_name, context))
51
52     def get_context(self, attrs):
53         return {
54             'items': self.items,
55             'name': self.name,
56             'show_from_noentry': self.show_from_noentry,
57             'show_x_results': self.show_x_results,
58             'results_scrollable': self.results_scrollable,
59             'selectable_limit': self.selectable_limit,
60             'placeholder': self.placeholder,
61             'initial': self.initial,
62         }
63
64
65 class SearchableSelectMultipleField(forms.Field):
66     def __init__(self, *args, required=True, widget=None, label=None, disabled=False,
67                  items=None, queryset=None, show_from_noentry=True, show_x_results=-1,
68                  results_scrollable=False, selectable_limit=-1, placeholder="search here",
69                  name="searchable_select", initial=[], **kwargs):
70         """
71         From the documentation.
72
73         # required -- Boolean that specifies whether the field is required.
74         #             True by default.
75         # widget -- A Widget class, or instance of a Widget class, that should
76         #           be used for this Field when displaying it. Each Field has a
77         #           default Widget that it'll use if you don't specify this. In
78         #           most cases, the default widget is TextInput.
79         # label -- A verbose name for this field, for use in displaying this
80         #          field in a form. By default, Django will use a "pretty"
81         #          version of the form field name, if the Field is part of a
82         #          Form.
83         # initial -- A value to use in this Field's initial display. This value
84         #            is *not* used as a fallback if data isn't given.
85         # help_text -- An optional string to use as "help text" for this Field.
86         # error_messages -- An optional dictionary to override the default
87         #                   messages that the field will raise.
88         # show_hidden_initial -- Boolean that specifies if it is needed to render a
89         #                        hidden widget with initial value after widget.
90         # validators -- List of additional validators to use
91         # localize -- Boolean that specifies if the field should be localized.
92         # disabled -- Boolean that specifies whether the field is disabled, that
93         #             is its widget is shown in the form but not editable.
94         # label_suffix -- Suffix to be added to the label. Overrides
95         #                 form's label_suffix.
96         """
97         self.widget = widget
98         if self.widget is None:
99             self.widget = SearchableSelectMultipleWidget(
100                 attrs={
101                     'items': items,
102                     'initial': [obj.id for obj in initial],
103                     'show_from_noentry': show_from_noentry,
104                     'show_x_results': show_x_results,
105                     'results_scrollable': results_scrollable,
106                     'selectable_limit': selectable_limit,
107                     'placeholder': placeholder,
108                     'name': name,
109                     'disabled': disabled
110                 }
111             )
112         self.disabled = disabled
113         self.queryset = queryset
114         self.selectable_limit = selectable_limit
115
116         super().__init__(disabled=disabled, **kwargs)
117
118         self.required = required
119
120     def clean(self, data):
121         data = data[0]
122         if not data:
123             if self.required:
124                 raise ValidationError("Nothing was selected")
125             else:
126                 return []
127         try:
128             data_as_list = json.loads(data)
129         except json.decoder.JSONDecodeError:
130             data_as_list = None
131         if not data_as_list:
132             raise ValidationError("Contents Not JSON")
133         if self.selectable_limit != -1:
134             if len(data_as_list) > self.selectable_limit:
135                 raise ValidationError("Too many items were selected")
136
137         items = []
138         for elem in data_as_list:
139             items.append(self.queryset.get(id=elem))
140
141         return items
142
143
144 class SearchableSelectAbstractForm(forms.Form):
145     def __init__(self, *args, queryset=None, initial=[], **kwargs):
146         self.queryset = queryset
147         items = self.generate_items(self.queryset)
148         options = self.generate_options()
149
150         super(SearchableSelectAbstractForm, self).__init__(*args, **kwargs)
151         self.fields['searchable_select'] = SearchableSelectMultipleField(
152             initial=initial,
153             items=items,
154             queryset=self.queryset,
155             **options
156         )
157
158     def get_validated_bundle(self):
159         bundles = self.cleaned_data['searchable_select']
160         if len(bundles) < 1:  # don't need to check for >1, as field does that for us
161             raise ValidationError("No bundle was selected")
162         return bundles[0]
163
164     def generate_items(self, queryset):
165         raise Exception("SearchableSelectAbstractForm does not implement concrete generate_items()")
166
167     def generate_options(self, disabled=False):
168         return {
169             'show_from_noentry': True,
170             'show_x_results': -1,
171             'results_scrollable': True,
172             'selectable_limit': 1,
173             'placeholder': 'Search for a Bundle',
174             'name': 'searchable_select',
175             'disabled': False
176         }
177
178
179 class SWConfigSelectorForm(SearchableSelectAbstractForm):
180     def generate_items(self, queryset):
181         items = {}
182
183         for bundle in queryset:
184             items[bundle.id] = {
185                 'expanded_name': bundle.name,
186                 'small_name': bundle.owner.username,
187                 'string': bundle.description,
188                 'id': bundle.id
189             }
190
191         return items
192
193
194 class OPNFVSelectForm(SearchableSelectAbstractForm):
195     def generate_items(self, queryset):
196         items = {}
197
198         for config in queryset:
199             items[config.id] = {
200                 'expanded_name': config.name,
201                 'small_name': config.bundle.owner.username,
202                 'string': config.description,
203                 'id': config.id
204             }
205
206         return items
207
208
209 class ResourceSelectorForm(SearchableSelectAbstractForm):
210     def generate_items(self, queryset):
211         items = {}
212
213         for bundle in queryset:
214             items[bundle.id] = {
215                 'expanded_name': bundle.name,
216                 'small_name': bundle.owner.username,
217                 'string': bundle.description,
218                 'id': bundle.id
219             }
220
221         return items
222
223
224 class BookingMetaForm(forms.Form):
225     # Django Form class for Book a Pod
226     length = forms.IntegerField(
227         widget=NumberInput(
228             attrs={
229                 "type": "range",
230                 'min': "1",
231                 "max": "21",
232                 "value": "1"
233             }
234         )
235     )
236     purpose = forms.CharField(max_length=1000)
237     project = forms.CharField(max_length=400)
238     info_file = forms.CharField(max_length=1000, required=False)
239     deploy_opnfv = forms.BooleanField(required=False)
240
241     def __init__(self, *args, user_initial=[], owner=None, **kwargs):
242         super(BookingMetaForm, self).__init__(**kwargs)
243
244         self.fields['users'] = SearchableSelectMultipleField(
245             queryset=UserProfile.objects.select_related('user').exclude(user=owner),
246             initial=user_initial,
247             items=get_user_items(exclude=owner),
248             required=False,
249             **get_user_field_opts()
250         )
251
252
253 class MultipleSelectFilterWidget(forms.Widget):
254     def __init__(self, *args, display_objects=None, filter_items=None, neighbors=None, **kwargs):
255         super(MultipleSelectFilterWidget, self).__init__(*args, **kwargs)
256         self.display_objects = display_objects
257         self.filter_items = filter_items
258         self.neighbors = neighbors
259         self.template_name = "dashboard/multiple_select_filter_widget.html"
260
261     def render(self, name, value, attrs=None, renderer=None):
262         context = self.get_context(name, value, attrs)
263         html = render_to_string(self.template_name, context=context)
264         return mark_safe(html)
265
266     def get_context(self, name, value, attrs):
267         return {
268             'display_objects': self.display_objects,
269             'neighbors': self.neighbors,
270             'filter_items': self.filter_items,
271             'initial_value': value
272         }
273
274
275 class MultipleSelectFilterField(forms.Field):
276
277     def __init__(self, **kwargs):
278         self.initial = kwargs.get("initial")
279         super().__init__(**kwargs)
280
281     def to_python(self, value):
282         try:
283             return json.loads(value)
284         except json.decoder.JSONDecodeError:
285             pass
286         raise ValidationError("content is not valid JSON")
287
288
289 class FormUtils:
290     @staticmethod
291     def getLabData(multiple_hosts=False, user=None):
292         """
293         Get all labs and thier host profiles, returns a serialized version the form can understand.
294
295         Could be rewritten with a related query to make it faster
296         """
297         # javascript truthy variables
298         true = 1
299         false = 0
300         if multiple_hosts:
301             multiple_hosts = true
302         else:
303             multiple_hosts = false
304         labs = {}
305         resources = {}
306         items = {}
307         neighbors = {}
308         for lab in Lab.objects.all():
309             lab_node = {
310                 'id': "lab_" + str(lab.lab_user.id),
311                 'model_id': lab.lab_user.id,
312                 'name': lab.name,
313                 'description': lab.description,
314                 'selected': false,
315                 'selectable': true,
316                 'follow': multiple_hosts,
317                 'multiple': false,
318                 'class': 'lab',
319                 'available_resources': json.dumps(lab.get_available_resources())
320             }
321
322             items[lab_node['id']] = lab_node
323             neighbors[lab_node['id']] = []
324             labs[lab_node['id']] = lab_node
325
326             for template in ResourceManager.getInstance().getAvailableResourceTemplates(lab, user):
327                 resource_node = {
328                     'form': {"name": "host_name", "type": "text", "placeholder": "hostname"},
329                     'id': "resource_" + str(template.id),
330                     'model_id': template.id,
331                     'name': template.name,
332                     'description': template.description,
333                     'selected': false,
334                     'selectable': true,
335                     'follow': false,
336                     'multiple': multiple_hosts,
337                     'class': 'resource',
338                     'required_resources': json.dumps(template.get_required_resources())
339                 }
340
341                 if multiple_hosts:
342                     resource_node['values'] = []  # place to store multiple values
343
344                 items[resource_node['id']] = resource_node
345                 neighbors[lab_node['id']].append(resource_node['id'])
346
347                 if resource_node['id'] not in neighbors:
348                     neighbors[resource_node['id']] = []
349
350                 neighbors[resource_node['id']].append(lab_node['id'])
351                 resources[resource_node['id']] = resource_node
352
353         display_objects = [("lab", labs.values()), ("resource", resources.values())]
354
355         context = {
356             'display_objects': display_objects,
357             'neighbors': neighbors,
358             'filter_items': items
359         }
360
361         return context
362
363
364 class HardwareDefinitionForm(forms.Form):
365
366     def __init__(self, user, *args, **kwargs):
367         super(HardwareDefinitionForm, self).__init__(*args, **kwargs)
368         attrs = FormUtils.getLabData(multiple_hosts=True, user=user)
369         self.fields['filter_field'] = MultipleSelectFilterField(
370             widget=MultipleSelectFilterWidget(**attrs)
371         )
372
373
374 class PodDefinitionForm(forms.Form):
375
376     fields = ["xml"]
377     xml = forms.CharField()
378
379
380 class ResourceMetaForm(forms.Form):
381
382     bundle_name = forms.CharField(label="POD Name")
383     bundle_description = forms.CharField(label="POD Description", widget=forms.Textarea, max_length=1000)
384
385
386 class GenericHostMetaForm(forms.Form):
387
388     host_profile = forms.CharField(label="Host Type", disabled=True, required=False)
389     host_name = forms.CharField(label="Host Name")
390
391
392 class NetworkDefinitionForm(forms.Form):
393     def __init__(self, *args, **kwargs):
394         super(NetworkDefinitionForm, self).__init__(**kwargs)
395
396
397 class NetworkConfigurationForm(forms.Form):
398     def __init__(self, *args, **kwargs):
399         super(NetworkConfigurationForm).__init__(**kwargs)
400
401
402 class HostSoftwareDefinitionForm(forms.Form):
403     # Django Form class for Design a Pod
404     host_name = forms.CharField(
405         max_length=200,
406         disabled=False,
407         required=True
408     )
409     headnode = forms.BooleanField(required=False, widget=forms.HiddenInput)
410
411     def __init__(self, *args, **kwargs):
412         imageQS = kwargs.pop("imageQS")
413         super(HostSoftwareDefinitionForm, self).__init__(*args, **kwargs)
414         self.fields['image'] = forms.ModelChoiceField(queryset=imageQS)
415
416
417 class WorkflowSelectionForm(forms.Form):
418     fields = ['workflow']
419
420     empty_permitted = False
421
422     workflow = forms.ChoiceField(
423         choices=(
424             (0, 'Booking'),
425             (1, 'Resource Bundle'),
426             (2, 'Software Configuration')
427         ),
428         label="Choose Workflow",
429         initial='booking',
430         required=True
431     )
432
433
434 class SnapshotHostSelectForm(forms.Form):
435     host = forms.CharField()
436
437
438 class BasicMetaForm(forms.Form):
439     name = forms.CharField()
440     description = forms.CharField(widget=forms.Textarea)
441
442
443 class ConfirmationForm(forms.Form):
444     fields = ['confirm']
445
446     confirm = forms.ChoiceField(
447         choices=(
448             (False, "Cancel"),
449             (True, "Confirm")
450         )
451     )
452
453
454 def validate_step(value):
455     if value not in ["prev", "next", "current"]:
456         raise ValidationError(str(value) + " is not allowed")
457
458
459 def validate_step_form(value):
460     try:
461         urllib.parse.unquote_plus(value)
462     except Exception:
463         raise ValidationError("Value is not url encoded data")
464
465
466 class ManagerForm(forms.Form):
467     step = forms.CharField(widget=forms.widgets.HiddenInput, validators=[validate_step])
468     step_form = forms.CharField(widget=forms.widgets.HiddenInput, validators=[validate_step_form])
469     # other fields?
470
471
472 class OPNFVSelectionForm(forms.Form):
473     installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=True)
474     scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=True)
475
476
477 class OPNFVNetworkRoleForm(forms.Form):
478     role = forms.CharField(max_length=200, disabled=True, required=False)
479
480     def __init__(self, *args, config_bundle, **kwargs):
481         super(OPNFVNetworkRoleForm, self).__init__(*args, **kwargs)
482         self.fields['network'] = forms.ModelChoiceField(
483             queryset=config_bundle.bundle.networks.all()
484         )
485
486
487 class OPNFVHostRoleForm(forms.Form):
488     host_name = forms.CharField(max_length=200, disabled=True, required=False)
489     role = forms.ModelChoiceField(queryset=OPNFVRole.objects.all().order_by("name").distinct("name"))