diff options
-rw-r--r-- | src/account/models.py | 8 | ||||
-rw-r--r-- | src/account/tasks.py | 5 | ||||
-rw-r--r-- | src/account/views.py | 6 | ||||
-rw-r--r-- | src/resource_inventory/models.py | 5 | ||||
-rw-r--r-- | src/static/js/dashboard.js | 97 | ||||
-rw-r--r-- | src/workflow/forms.py | 15 | ||||
-rw-r--r-- | src/workflow/models.py | 2 |
7 files changed, 132 insertions, 6 deletions
diff --git a/src/account/models.py b/src/account/models.py index 294e109..4aab306 100644 --- a/src/account/models.py +++ b/src/account/models.py @@ -10,9 +10,11 @@ from django.contrib.auth.models import User from django.db import models +from django.apps import apps import json import random +from collections import Counter class LabStatus(object): """ @@ -212,6 +214,12 @@ class Lab(models.Model): key += random.choice(alphabet) return key + def get_available_resources(self): + # Cannot import model normally due to ciruclar import + Server = apps.get_model('resource_inventory', 'Server') # TODO: Find way to import ResourceQuery + resources = [str(resource.profile) for resource in Server.objects.filter(lab=self, booked=False)] + return dict(Counter(resources)) + def __str__(self): return self.name diff --git a/src/account/tasks.py b/src/account/tasks.py index fe51974..53fbaf5 100644 --- a/src/account/tasks.py +++ b/src/account/tasks.py @@ -26,7 +26,10 @@ def sync_jira_accounts(): except JIRAError: # User can be anonymous (local django admin account) continue - user.email = user_dict['emailAddress'] + try: + user.email = user_dict['emailAddress'] + except: + pass user.userprofile.url = user_dict['self'] user.userprofile.full_name = user_dict['displayName'] diff --git a/src/account/views.py b/src/account/views.py index d1cc813..1cb2275 100644 --- a/src/account/views.py +++ b/src/account/views.py @@ -127,7 +127,11 @@ class JiraAuthenticatedView(RedirectView): jira = JIRA(server=settings.JIRA_URL, oauth=oauth_dict) username = jira.current_user() - email = jira.user(username).emailAddress + email = "" + try: + email = jira.user(username).emailAddress + except: + email = "" url = '/' # Step 3. Lookup the user or create them if they don't exist. try: diff --git a/src/resource_inventory/models.py b/src/resource_inventory/models.py index 557a4fc..4a6375d 100644 --- a/src/resource_inventory/models.py +++ b/src/resource_inventory/models.py @@ -14,6 +14,7 @@ from django.db import models from django.db.models import Q import re +from collections import Counter from account.models import Lab from dashboard.utils import AbstractModelQuery @@ -169,6 +170,10 @@ class ResourceTemplate(models.Model): configs = self.resourceConfigurations.all() return list(configs) + def get_required_resources(self): + profiles = Counter([str(config.profile) for config in self.getConfigs()]) + return dict(profiles) + def __str__(self): return self.name diff --git a/src/static/js/dashboard.js b/src/static/js/dashboard.js index 10c7d84..efc0542 100644 --- a/src/static/js/dashboard.js +++ b/src/static/js/dashboard.js @@ -209,6 +209,8 @@ class MultipleSelectFilterWidget { this.inputs = []; this.graph_neighbors = neighbors; this.filter_items = items; + this.currentLab = null; + this.available_resources = {}; this.result = {}; this.dropdown_count = 0; @@ -220,7 +222,7 @@ class MultipleSelectFilterWidget { this.make_selection(initial); } - make_selection( initial_data ){ + make_selection(initial_data){ if(!initial_data || jQuery.isEmptyObject(initial_data)) return; for(let item_class in initial_data) { @@ -257,9 +259,11 @@ class MultipleSelectFilterWidget { const toCheck = [root]; while(toCheck.length > 0){ const node = toCheck.pop(); + if(!node['marked']) { continue; //already visited, just continue } + node['marked'] = false; //mark as visited if(node['follow'] || node == root){ //add neighbors if we want to follow this node const neighbors = this.graph_neighbors[node.id]; @@ -305,6 +309,10 @@ class MultipleSelectFilterWidget { node['selected'] = true; elem.classList.remove('bg-white', 'not-allowed', 'bg-light'); elem.classList.add('selected_node'); + + if(node['class'] == 'resource') + this.reserveResource(node); + } clear(node) { @@ -323,11 +331,96 @@ class MultipleSelectFilterWidget { elem.classList.add('not-allowed', 'bg-light'); } + labCheck(node){ + // if lab is not already selected update available resources + if(!node['selected']) { + this.currentLab = node; + this.available_resources = JSON.parse(node['available_resources']); + this.updateAvailibility(); + } else { + // a lab is already selected, clear already selected resources + if(confirm('Unselecting a lab will reset all selected resources, are you sure?')) + location.reload(); + } + } + + updateAvailibility() { + const lab_resources = this.graph_neighbors[this.currentLab.id]; + + // need to loop through and update all quantities + for(let i in lab_resources) { + const resource_node = this.filter_items[lab_resources[i]]; + const required_resources = JSON.parse(resource_node['required_resources']); + let elem = document.getElementById(resource_node.id).getElementsByClassName("grid-item-description")[0]; + let leastAvailable = 100; + let currCount; + let quantityDescription; + let quantityNode; + + // console.log(this.available_resources); + for(let resource in required_resources) { + currCount = Math.floor(this.available_resources[resource] / required_resources[resource]); + if(currCount < leastAvailable) + leastAvailable = currCount; + + if(!currCount || currCount < 0) { + leastAvailable = 0 + break; + } + } + + if (elem.children[0]){ + elem.removeChild(elem.children[0]); + } + + quantityDescription = '<br> Quantity Currently Available: ' + leastAvailable; + quantityNode = document.createElement('P'); + if (leastAvailable > 0) { + quantityDescription = quantityDescription.fontcolor('green'); + } else { + quantityDescription = quantityDescription.fontcolor('red'); + } + + quantityNode.innerHTML = quantityDescription; + elem.appendChild(quantityNode) + } + } + + reserveResource(node){ + const required_resources = JSON.parse(node['required_resources']); + + for(let resource in required_resources){ + this.available_resources[resource] -= required_resources[resource]; + } + + this.updateAvailibility(); + } + + releaseResource(node){ + const required_resources = JSON.parse(node['required_resources']); + + for(let resource in required_resources){ + this.available_resources[resource] += required_resources[resource]; + } + + this.updateAvailibility(); + } + processClick(id){ const node = this.filter_items[id]; if(!node['selectable']) return; + // If they are selecting a lab, update accordingly + if (node['class'] == 'lab') + this.labCheck(node); + + // Can only select a resource if a lab is selected + if (!this.currentLab) { + alert('You must select a lab before selecting a resource'); + return; + } + if(node['multiple']){ return this.processClickMultiple(node); } else { @@ -341,6 +434,7 @@ class MultipleSelectFilterWidget { this.select(node); } else { this.clear(node); + this.releaseResource(node); // can't do this in clear since clear removes border } this.process(node); this.updateResult(node); @@ -423,6 +517,7 @@ class MultipleSelectFilterWidget { const parent = div.parentNode; div.parentNode.removeChild(div); this.result[node.class][node.id]['count']--; + this.releaseResource(node); // This can't be done on clear b/c clear removes border //checks if we have removed last item in class if(this.result[node.class][node.id]['count'] == 0){ diff --git a/src/workflow/forms.py b/src/workflow/forms.py index 4220dea..9b56f93 100644 --- a/src/workflow/forms.py +++ b/src/workflow/forms.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. +# Copyright (c) 2020 Sawyer Bergeron, Sean Smith, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -22,7 +23,7 @@ from account.models import UserProfile from resource_inventory.models import ( OPNFVRole, Installer, - Scenario, + Scenario ) from resource_inventory.resource_manager import ResourceManager from booking.lib import get_user_items, get_user_field_opts @@ -314,8 +315,10 @@ class FormUtils: 'selectable': true, 'follow': multiple_hosts, 'multiple': false, - 'class': 'lab' + 'class': 'lab', + 'available_resources': json.dumps(lab.get_available_resources()) } + items[lab_node['id']] = lab_node neighbors[lab_node['id']] = [] labs[lab_node['id']] = lab_node @@ -331,14 +334,19 @@ class FormUtils: 'selectable': true, 'follow': false, 'multiple': multiple_hosts, - 'class': 'resource' + 'class': 'resource', + 'required_resources': json.dumps(template.get_required_resources()) } + if multiple_hosts: resource_node['values'] = [] # place to store multiple values + items[resource_node['id']] = resource_node neighbors[lab_node['id']].append(resource_node['id']) + if resource_node['id'] not in neighbors: neighbors[resource_node['id']] = [] + neighbors[resource_node['id']].append(lab_node['id']) resources[resource_node['id']] = resource_node @@ -349,6 +357,7 @@ class FormUtils: 'neighbors': neighbors, 'filter_items': items } + return context diff --git a/src/workflow/models.py b/src/workflow/models.py index 4a5616e..f550a38 100644 --- a/src/workflow/models.py +++ b/src/workflow/models.py @@ -11,6 +11,7 @@ from django.template.loader import get_template from django.http import HttpResponse from django.utils import timezone +from django.db import transaction import yaml import requests @@ -559,6 +560,7 @@ class Repository(): self.el[self.RESULT] = bundle return False + @transaction.atomic # TODO: Rewrite transactions with savepoints at user level for all workflows def make_booking(self): models = self.el[self.BOOKING_MODELS] owner = self.el[self.SESSION_USER] |