Merge "Hides expired bookings in the "My Bookings" Page"
authorParker Berberian <pberberian@iol.unh.edu>
Wed, 13 Mar 2019 17:20:26 +0000 (17:20 +0000)
committerGerrit Code Review <gerrit@opnfv.org>
Wed, 13 Mar 2019 17:20:26 +0000 (17:20 +0000)
30 files changed:
__init__.py [deleted file]
src/__init__.py [deleted file]
src/account/tests/test_general.py
src/api/models.py
src/api/tests/test_serializers.py [deleted file]
src/api/urls.py
src/api/views.py
src/booking/quick_deployer.py
src/booking/tests/test_models.py
src/booking/tests/test_quick_booking.py [new file with mode: 0644]
src/dashboard/exceptions.py
src/dashboard/testing_utils.py [new file with mode: 0644]
src/notifier/manager.py
src/notifier/migrations/0003_auto_20190123_1741.py [new file with mode: 0644]
src/notifier/migrations/0004_auto_20190124_2115.py [new file with mode: 0644]
src/notifier/migrations/0005_auto_20190306_1616.py [new file with mode: 0644]
src/notifier/models.py
src/notifier/views.py
src/resource_inventory/migrations/0007_auto_20190306_1616.py [new file with mode: 0644]
src/resource_inventory/migrations/0008_host_remote_management.py [new file with mode: 0644]
src/resource_inventory/models.py
src/resource_inventory/pdf_templater.py [new file with mode: 0644]
src/resource_inventory/resource_manager.py
src/templates/dashboard/idf.yaml [new file with mode: 0644]
src/templates/dashboard/pdf.yaml
src/templates/notifier/inbox.html
src/templates/notifier/notification.html
src/templates/workflow/viewport-base.html
src/workflow/models.py
test.sh

diff --git a/__init__.py b/__init__.py
deleted file mode 100644 (file)
index b6fef6c..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-##############################################################################
-# Copyright (c) 2016 Max Breitenfeldt 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
-##############################################################################
diff --git a/src/__init__.py b/src/__init__.py
deleted file mode 100644 (file)
index b6fef6c..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-##############################################################################
-# Copyright (c) 2016 Max Breitenfeldt 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
-##############################################################################
index 57ad291..3fb52b0 100644 (file)
@@ -47,7 +47,7 @@ class AccountMiddlewareTestCase(TestCase):
         self.user1profile.timezone = 'Etc/Greenwich'
         self.user1profile.save()
         self.client.get(url)
-        self.assertEqual(timezone.get_current_timezone_name(), 'Etc/Greenwich')
+        self.assertEqual(timezone.get_current_timezone_name(), 'GMT')
 
         # if there is no profile for a user, it should be created
         user2 = User.objects.create(username='user2')
index 30f0f75..b35adf2 100644 (file)
@@ -11,6 +11,7 @@
 from django.contrib.auth.models import User
 from django.db import models
 from django.core.exceptions import PermissionDenied
+from django.shortcuts import get_object_or_404
 
 import json
 import uuid
@@ -21,7 +22,8 @@ from resource_inventory.models import (
     HostProfile,
     Host,
     Image,
-    Interface
+    Interface,
+    RemoteInfo
 )
 
 
@@ -60,6 +62,32 @@ class LabManager(object):
     def __init__(self, lab):
         self.lab = lab
 
+    def update_host_remote_info(self, data, host_id):
+        host = get_object_or_404(Host, labid=host_id, lab=self.lab)
+        info = {}
+        try:
+            info['address'] = data['address']
+            info['mac_address'] = data['mac_address']
+            info['password'] = data['password']
+            info['user'] = data['user']
+            info['type'] = data['type']
+            info['versions'] = json.dumps(data['versions'])
+        except Exception as e:
+            return {"error": "invalid arguement: " + str(e)}
+        remote_info = host.remote_management
+        if "default" in remote_info.mac_address:
+            remote_info = RemoteInfo()
+        remote_info.address = info['address']
+        remote_info.mac_address = info['mac_address']
+        remote_info.password = info['password']
+        remote_info.user = info['user']
+        remote_info.type = info['type']
+        remote_info.versions = info['versions']
+        remote_info.save()
+        host.remote_management = remote_info
+        host.save()
+        return {"status": "success"}
+
     def get_profile(self):
         prof = {}
         prof['name'] = self.lab.name
@@ -88,6 +116,22 @@ class LabManager(object):
         inventory['host_types'] = self.serialize_host_profiles(profiles)
         return inventory
 
+    def get_host(self, hostname):
+        host = get_object_or_404(Host, labid=hostname, lab=self.lab)
+        return {
+            "booked": host.booked,
+            "working": host.working,
+            "type": host.profile.name
+        }
+
+    def update_host(self, hostname, data):
+        host = get_object_or_404(Host, labid=hostname, lab=self.lab)
+        if "working" in data:
+            working = data['working'] == "true"
+            host.working = working
+        host.save()
+        return self.get_host(hostname)
+
     def get_status(self):
         return {"status": self.lab.status}
 
diff --git a/src/api/tests/test_serializers.py b/src/api/tests/test_serializers.py
deleted file mode 100644 (file)
index c1fa5af..0000000
+++ /dev/null
@@ -1,229 +0,0 @@
-##############################################################################
-# Copyright (c) 2018 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 django.test import TestCase
-from booking.models import Booking
-from account.models import Lab
-from api.serializers.booking_serializer import BookingField
-from datetime import timedelta
-from django.utils import timezone
-from django.contrib.auth.models import Permission, User
-from resource_inventory.models import (
-    Image,
-    OPNFVRole,
-    HostConfiguration,
-    HostProfile,
-    InterfaceProfile,
-    DiskProfile,
-    CpuProfile,
-    RamProfile,
-    GenericResourceBundle,
-    GenericResource,
-    GenericHost,
-    Host,
-    Vlan,
-    Interface,
-    ConfigBundle,
-    ResourceBundle
-)
-
-
-class BookingSerializerTestCase(TestCase):
-
-    count = 0
-
-    def makeHostConfigurations(self, hosts, config):
-        lab_user = User.objects.create(username="asfasdfasdf")
-        owner = User.objects.create(username="asfasdfasdffffff")
-        lab = Lab.objects.create(
-            lab_user=lab_user,
-            name="TestLab123123",
-            contact_email="mail@email.com",
-            contact_phone=""
-        )
-        jumphost = True
-        for host in hosts:
-            image = Image.objects.create(
-                lab_id=12,
-                from_lab=lab,
-                name="this is a test image",
-                owner=owner
-            )
-            name = "jumphost"
-            if not jumphost:
-                name = "compute"
-            role = OPNFVRole.objects.create(
-                name=name,
-                description="stuff"
-            )
-
-            HostConfiguration.objects.create(
-                host=host,
-                image=image,
-                bundle=config,
-                opnfvRole=role
-            )
-            jumphost = False
-
-    def setUp(self):
-        self.serializer = BookingField()
-        lab_user = User.objects.create(username="lab user")
-        lab = Lab.objects.create(name="test lab", lab_user=lab_user)
-        # create hostProfile
-        hostProfile = HostProfile.objects.create(
-            host_type=0,
-            name='Test profile',
-            description='a test profile'
-        )
-        InterfaceProfile.objects.create(
-            speed=1000,
-            name='eno3',
-            host=hostProfile
-        )
-        DiskProfile.objects.create(
-            size=1000,
-            media_type="SSD",
-            name='/dev/sda',
-            host=hostProfile
-        )
-        CpuProfile.objects.create(
-            cores=96,
-            architecture="x86_64",
-            cpus=2,
-            host=hostProfile
-        )
-        RamProfile.objects.create(
-            amount=256,
-            channels=4,
-            host=hostProfile
-        )
-
-        # create GenericResourceBundle
-        genericBundle = GenericResourceBundle.objects.create()
-
-        gres1 = GenericResource.objects.create(
-            bundle=genericBundle,
-            name='generic resource ' + str(self.count)
-        )
-        self.count += 1
-        gHost1 = GenericHost.objects.create(
-            resource=gres1,
-            profile=hostProfile
-        )
-
-        gres2 = GenericResource.objects.create(
-            bundle=genericBundle,
-            name='generic resource ' + str(self.count)
-        )
-        self.count += 1
-        gHost2 = GenericHost.objects.create(
-            resource=gres2,
-            profile=hostProfile
-        )
-        user1 = User.objects.create(username='user1')
-
-        add_booking_perm = Permission.objects.get(codename='add_booking')
-        user1.user_permissions.add(add_booking_perm)
-
-        user1 = User.objects.get(pk=user1.id)
-
-        conf = ConfigBundle.objects.create(owner=user1, name="test conf")
-        self.makeHostConfigurations([gHost1, gHost2], conf)
-
-        # actual resource bundle
-        bundle = ResourceBundle.objects.create(
-            template=genericBundle
-        )
-
-        host1 = Host.objects.create(
-            template=gHost1,
-            booked=True,
-            name='host1',
-            bundle=bundle,
-            profile=hostProfile,
-            lab=lab
-        )
-
-        host2 = Host.objects.create(
-            template=gHost2,
-            booked=True,
-            name='host2',
-            bundle=bundle,
-            profile=hostProfile,
-            lab=lab
-        )
-
-        vlan1 = Vlan.objects.create(vlan_id=300, tagged=False)
-        vlan2 = Vlan.objects.create(vlan_id=300, tagged=False)
-
-        iface1 = Interface.objects.create(
-            mac_address='00:11:22:33:44:55',
-            bus_address='some bus address',
-            switch_name='switch1',
-            port_name='port10',
-            host=host1
-        )
-
-        iface1.config = [vlan1]
-
-        iface2 = Interface.objects.create(
-            mac_address='00:11:22:33:44:56',
-            bus_address='some bus address',
-            switch_name='switch1',
-            port_name='port12',
-            host=host2
-        )
-
-        iface2.config = [vlan2]
-
-        # finally, can create booking
-        self.booking = Booking.objects.create(
-            owner=user1,
-            start=timezone.now(),
-            end=timezone.now() + timedelta(weeks=1),
-            purpose='Testing',
-            resource=bundle,
-            config_bundle=conf
-        )
-
-        serialized_booking = {}
-
-        host1 = {}
-        host1['hostname'] = 'host1'
-        host1['image'] = {}  # TODO: Images
-        host1['deploy_image'] = True
-        host2 = {}
-        host2['hostname'] = 'host2'
-        host2['image'] = {}  # TODO: Images
-        host2['deploy_image'] = True
-
-        serialized_booking['hosts'] = [host1, host2]
-
-        net = {}
-        net['name'] = 'network_name'
-        net['vlan_id'] = 300
-        netHost1 = {}
-        netHost1['hostname'] = 'host1'
-        netHost1['tagged'] = False
-        netHost1['interface'] = 0
-        netHost2 = {}
-        netHost2['hostname'] = 'host2'
-        netHost2['tagged'] = False
-        netHost2['interface'] = 0
-        net['hosts'] = [netHost1, netHost2]
-
-        serialized_booking['networking'] = [net]
-        serialized_booking['jumphost'] = 'host1'
-
-        self.serialized_booking = serialized_booking
-
-    def test_to_representation(self):
-        keys = ['hosts', 'networking', 'jumphost']
-        serialized_form = self.serializer.to_representation(self.booking)
-        for key in keys:
-            self.assertEquals(serialized_form[key], self.serialized_booking)
index 50cc6ac..d18a04d 100644 (file)
@@ -39,6 +39,8 @@ from api.views import (
     new_jobs,
     current_jobs,
     done_jobs,
+    update_host_bmc,
+    lab_host,
     GenerateTokenView
 )
 
@@ -51,6 +53,8 @@ urlpatterns = [
     path('labs/<slug:lab_name>/profile', lab_profile),
     path('labs/<slug:lab_name>/status', lab_status),
     path('labs/<slug:lab_name>/inventory', lab_inventory),
+    path('labs/<slug:lab_name>/hosts/<slug:host_id>', lab_host),
+    path('labs/<slug:lab_name>/hosts/<slug:host_id>/bmc', update_host_bmc),
     path('labs/<slug:lab_name>/jobs/<int:job_id>', specific_job),
     path('labs/<slug:lab_name>/jobs/<int:job_id>/<slug:task_id>', specific_task),
     path('labs/<slug:lab_name>/jobs/new', new_jobs),
index c72c85c..a56dcfe 100644 (file)
@@ -54,6 +54,16 @@ def lab_inventory(request, lab_name=""):
     return JsonResponse(lab_manager.get_inventory(), safe=False)
 
 
+@csrf_exempt
+def lab_host(request, lab_name="", host_id=""):
+    lab_token = request.META.get('HTTP_AUTH_TOKEN')
+    lab_manager = LabManagerTracker.get(lab_name, lab_token)
+    if request.method == "GET":
+        return JsonResponse(lab_manager.get_host(host_id), safe=False)
+    if request.method == "POST":
+        return JsonResponse(lab_manager.update_host(host_id, request.POST), safe=False)
+
+
 def lab_status(request, lab_name=""):
     lab_token = request.META.get('HTTP_AUTH_TOKEN')
     lab_manager = LabManagerTracker.get(lab_name, lab_token)
@@ -62,6 +72,18 @@ def lab_status(request, lab_name=""):
     return JsonResponse(lab_manager.get_status(), safe=False)
 
 
+@csrf_exempt
+def update_host_bmc(request, lab_name="", host_id=""):
+    lab_token = request.META.get('HTTP_AUTH_TOKEN')
+    lab_manager = LabManagerTracker.get(lab_name, lab_token)
+    if request.method == "POST":
+        # update / create RemoteInfo for host
+        return JsonResponse(
+            lab_manager.update_host_remote_info(request.POST, host_id),
+            safe=False
+        )
+
+
 def lab_profile(request, lab_name=""):
     lab_token = request.META.get('HTTP_AUTH_TOKEN')
     lab_manager = LabManagerTracker.get(lab_name, lab_token)
@@ -93,6 +115,7 @@ def specific_task(request, lab_name="", job_id="", task_id=""):
         return JsonResponse(get_task(task_id).config.get_delta())
 
 
+@csrf_exempt
 def specific_job(request, lab_name="", job_id=""):
     lab_token = request.META.get('HTTP_AUTH_TOKEN')
     lab_manager = LabManagerTracker.get(lab_name, lab_token)
index d838de9..8a81d18 100644 (file)
@@ -33,11 +33,14 @@ from resource_inventory.models import (
     OPNFVConfig
 )
 from resource_inventory.resource_manager import ResourceManager
+from resource_inventory.pdf_templater import PDFTemplater
+from notifier.manager import NotificationHandler
 from booking.models import Booking
 from dashboard.exceptions import (
     InvalidHostnameException,
     ResourceAvailabilityException,
-    ModelValidationException
+    ModelValidationException,
+    BookingLengthException
 )
 from api.models import JobFactory
 
@@ -87,22 +90,8 @@ class NoRemainingPublicNetwork(Exception):
     pass
 
 
-def create_from_form(form, request):
-    quick_booking_id = str(uuid.uuid4())
-
-    host_field = form.cleaned_data['filter_field']
-    host_json = json.loads(host_field)
-    purpose_field = form.cleaned_data['purpose']
-    project_field = form.cleaned_data['project']
-    users_field = form.cleaned_data['users']
-    host_name = form.cleaned_data['hostname']
-    length = form.cleaned_data['length']
-
-    image = form.cleaned_data['image']
-    scenario = form.cleaned_data['scenario']
-    installer = form.cleaned_data['installer']
-
-    # get all initial info we need to validate
+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])
@@ -114,110 +103,185 @@ def create_from_form(form, request):
     profile = HostProfile.objects.get(id=profile_id)
 
     # check validity of field data before trying to apply to models
+    if len(host_json['labs']) != 1:
+        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")
 
-    # check that hostname is valid
-    if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", host_name):
-        raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
-    # check that image os is compatible with installer
-    if installer in image.os.sup_installers.all():
-        # if installer not here, we can omit that and not check for scenario
-        if not scenario:
-            raise IncompatibleScenarioForInstaller("An OPNFV Installer needs a scenario to be chosen to work properly")
-        if scenario not in installer.sup_scenarios.all():
-            raise IncompatibleScenarioForInstaller("The chosen installer does not support the chosen scenario")
-    if image.from_lab != lab:
-        raise ImageNotAvailableAtLab("The chosen image is not available at the chosen hosting lab")
-    if image.host_type != profile:
-        raise IncompatibleImageForHost("The chosen image is not available for the chosen host type")
-    if not image.public and image.owner != request.user:
-        raise ImageOwnershipInvalid("You are not the owner of the chosen private image")
+    return lab, profile
+
 
-    # check if host type is available
-    # ResourceManager.getInstance().acquireHost(ghost, lab.name)
+def check_available_matching_host(lab, hostprofile):
     available_host_types = ResourceManager.getInstance().getAvailableHostTypes(lab)
-    if profile not in available_host_types:
+    if hostprofile not in available_host_types:
         # TODO: handle deleting generic resource in this instance along with grb
         raise HostNotAvailable("Could not book selected host due to changed availability. Try again later")
 
-    # check if any hosts with profile at lab are still available
-    hostset = Host.objects.filter(lab=lab, profile=profile).filter(booked=False).filter(working=True)
-    if not hostset.first():
+    hostset = Host.objects.filter(lab=lab, profile=hostprofile).filter(booked=False).filter(working=True)
+    if not hostset.exists():
         raise HostNotAvailable("Couldn't find any matching unbooked hosts")
 
-    # generate GenericResourceBundle
-    if len(host_json['labs']) != 1:
-        raise NoLabSelectedError("No lab was selected")
+    return True
+
 
-    grbundle = GenericResourceBundle(owner=request.user)
+def generate_grb(owner, lab, common_id):
+    grbundle = GenericResourceBundle(owner=owner)
     grbundle.lab = lab
-    grbundle.name = "grbundle for quick booking with uid " + quick_booking_id
+    grbundle.name = "grbundle for quick booking with uid " + common_id
     grbundle.description = "grbundle created for quick-deploy booking"
     grbundle.save()
 
-    # generate GenericResource, GenericHost
-    gresource = GenericResource(bundle=grbundle, name=host_name)
+    return grbundle
+
+
+def generate_gresource(bundle, hostname):
+    if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", hostname):
+        raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
+    gresource = GenericResource(bundle=bundle, name=hostname)
     gresource.save()
 
+    return gresource
+
+
+def generate_ghost(generic_resource, host_profile):
     ghost = GenericHost()
-    ghost.resource = gresource
-    ghost.profile = profile
+    ghost.resource = generic_resource
+    ghost.profile = host_profile
     ghost.save()
 
-    # generate config bundle
+    return ghost
+
+
+def generate_config_bundle(owner, common_id, grbundle):
     cbundle = ConfigBundle()
-    cbundle.owner = request.user
-    cbundle.name = "configbundle for quick booking  with uid " + quick_booking_id
+    cbundle.owner = owner
+    cbundle.name = "configbundle for quick booking with uid " + common_id
     cbundle.description = "configbundle created for quick-deploy booking"
     cbundle.bundle = grbundle
     cbundle.save()
 
-    # generate OPNFVConfig pointing to cbundle
-    if installer:
-        opnfvconfig = OPNFVConfig()
-        opnfvconfig.scenario = scenario
-        opnfvconfig.installer = installer
-        opnfvconfig.bundle = cbundle
-        opnfvconfig.save()
+    return cbundle
+
+
+def generate_opnfvconfig(scenario, installer, config_bundle):
+    opnfvconfig = OPNFVConfig()
+    opnfvconfig.scenario = scenario
+    opnfvconfig.installer = installer
+    opnfvconfig.bundle = config_bundle
+    opnfvconfig.save()
+
+    return opnfvconfig
+
 
-    # generate HostConfiguration pointing to cbundle
+def generate_hostconfig(generic_host, image, config_bundle):
     hconf = HostConfiguration()
-    hconf.host = ghost
+    hconf.host = generic_host
     hconf.image = image
-    hconf.opnfvRole = OPNFVRole.objects.get(name="Jumphost")
-    if not hconf.opnfvRole:
-        raise OPNFVRoleDNE("No jumphost role was found")
-    hconf.bundle = cbundle
+
+    opnfvrole = OPNFVRole.objects.get(name="Jumphost")
+    if not opnfvrole:
+        raise OPNFVRoleDNE("No jumphost role was found.")
+
+    hconf.opnfvRole = opnfvrole
+    hconf.bundle = config_bundle
     hconf.save()
 
+    return hconf
+
+
+def generate_resource_bundle(generic_resource_bundle, config_bundle):  # warning: requires cleanup
+    try:
+        resource_manager = ResourceManager.getInstance()
+        resource_bundle = resource_manager.convertResourceBundle(generic_resource_bundle, config=config_bundle)
+        return resource_bundle
+    except ResourceAvailabilityException:
+        raise ResourceAvailabilityException("Requested resources not available")
+    except ModelValidationException:
+        raise ModelValidationException("Encountered error while saving grbundle")
+
+
+def check_invariants(request, **kwargs):
+    installer = kwargs['installer']
+    image = kwargs['image']
+    scenario = kwargs['scenario']
+    lab = kwargs['lab']
+    host_profile = kwargs['host_profile']
+    length = kwargs['length']
+    # check that image os is compatible with installer
+    if installer in image.os.sup_installers.all():
+        # if installer not here, we can omit that and not check for scenario
+        if not scenario:
+            raise IncompatibleScenarioForInstaller("An OPNFV Installer needs a scenario to be chosen to work properly")
+        if scenario not in installer.sup_scenarios.all():
+            raise IncompatibleScenarioForInstaller("The chosen installer does not support the chosen scenario")
+    if image.from_lab != lab:
+        raise ImageNotAvailableAtLab("The chosen image is not available at the chosen hosting lab")
+    if image.host_type != host_profile:
+        raise IncompatibleImageForHost("The chosen image is not available for the chosen host type")
+    if not image.public and image.owner != request.user:
+        raise ImageOwnershipInvalid("You are not the owner of the chosen private image")
+    if length < 1 or length > 21:
+        raise BookingLengthException("Booking must be between 1 and 21 days long")
+
+
+def create_from_form(form, request):
+    quick_booking_id = str(uuid.uuid4())
+
+    host_field = form.cleaned_data['filter_field']
+    purpose_field = form.cleaned_data['purpose']
+    project_field = form.cleaned_data['project']
+    users_field = form.cleaned_data['users']
+    hostname = form.cleaned_data['hostname']
+    length = form.cleaned_data['length']
+
+    image = form.cleaned_data['image']
+    scenario = form.cleaned_data['scenario']
+    installer = form.cleaned_data['installer']
+
+    lab, host_profile = parse_host_field(host_field)
+    data = form.cleaned_data
+    data['lab'] = lab
+    data['host_profile'] = host_profile
+    check_invariants(request, **data)
+
+    check_available_matching_host(lab, host_profile)  # requires cleanup if failure after this point
+
+    grbundle = generate_grb(request.user, lab, quick_booking_id)
+
+    gresource = generate_gresource(grbundle, hostname)
+
+    ghost = generate_ghost(gresource, host_profile)
+
+    cbundle = generate_config_bundle(request.user, quick_booking_id, grbundle)
+
+    # if no installer provided, just create blank host
+    if installer:
+        generate_opnfvconfig(scenario, installer, cbundle)
+
+    generate_hostconfig(ghost, image, cbundle)
+
     # construct generic interfaces
-    for interface_profile in profile.interfaceprofile.all():
+    for interface_profile in host_profile.interfaceprofile.all():
         generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
         generic_interface.save()
-    ghost.save()
 
     # get vlan, assign to first interface
     publicnetwork = lab.vlan_manager.get_public_vlan()
-    publicvlan = publicnetwork.vlan
     if not publicnetwork:
         raise NoRemainingPublicNetwork("No public networks were available for your pod")
+    publicvlan = publicnetwork.vlan
     lab.vlan_manager.reserve_public_vlan(publicvlan)
 
     vlan = Vlan.objects.create(vlan_id=publicvlan, tagged=False, public=True)
     vlan.save()
+
     ghost.generic_interfaces.first().vlans.add(vlan)
     ghost.generic_interfaces.first().save()
 
     # generate resource bundle
-    try:
-        resource_bundle = ResourceManager.getInstance().convertResourceBundle(grbundle, config=cbundle)
-    except ResourceAvailabilityException:
-        raise ResourceAvailabilityException("Requested resources not available")
-    except ModelValidationException:
-        raise ModelValidationException("Encountered error while saving grbundle")
+    resource_bundle = generate_resource_bundle(grbundle, cbundle)
 
     # generate booking
     booking = Booking()
@@ -228,7 +292,7 @@ def create_from_form(form, request):
     booking.start = timezone.now()
     booking.end = timezone.now() + timedelta(days=int(length))
     booking.resource = resource_bundle
-    booking.pdf = ResourceManager().makePDF(booking.resource)
+    booking.pdf = PDFTemplater.makePDF(booking.resource)
     booking.config_bundle = cbundle
     booking.save()
     users_field = users_field[2:-2]
@@ -241,6 +305,7 @@ def create_from_form(form, request):
 
     # generate job
     JobFactory.makeCompleteJob(booking)
+    NotificationHandler.notify_new_booking(booking)
 
 
 def drop_filter(user):
index c7fb25d..6170295 100644 (file)
@@ -230,10 +230,3 @@ class BookingModelTestCase(TestCase):
             booking.save()
         except Exception:
             self.fail("save() threw an exception")
-        booking.end = booking.end + timedelta(weeks=2)
-        self.assertRaises(ValueError, booking.save)
-        booking.end = booking.end - timedelta(days=8)
-        try:
-            self.assertTrue(booking.save())
-        except Exception:
-            self.fail("save() threw an exception")
diff --git a/src/booking/tests/test_quick_booking.py b/src/booking/tests/test_quick_booking.py
new file mode 100644 (file)
index 0000000..936a9a5
--- /dev/null
@@ -0,0 +1,155 @@
+##############################################################################
+# Copyright (c) 2018 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
+##############################################################################
+
+import datetime
+
+from django.test import TestCase, Client
+
+from booking.models import Booking
+from dashboard.testing_utils import (
+    instantiate_host,
+    instantiate_user,
+    instantiate_userprofile,
+    instantiate_lab,
+    instantiate_installer,
+    instantiate_image,
+    instantiate_scenario,
+    instantiate_os,
+    make_hostprofile_set,
+    instantiate_opnfvrole,
+    instantiate_publicnet,
+)
+# from dashboard import test_utils
+
+
+class QuickBookingValidFormTestCase(TestCase):
+    @classmethod
+    def setUpTestData(cls):
+        cls.loginuser = instantiate_user(False, username="newtestuser", password="testpassword")
+        instantiate_userprofile(cls.loginuser, True)
+
+        lab_user = instantiate_user(True)
+        cls.lab = instantiate_lab(lab_user)
+
+        cls.host_profile = make_hostprofile_set(cls.lab)
+        cls.scenario = instantiate_scenario()
+        cls.installer = instantiate_installer([cls.scenario])
+        os = instantiate_os([cls.installer])
+        cls.image = instantiate_image(cls.lab, 1, cls.loginuser, os, cls.host_profile)
+        cls.host = instantiate_host(cls.host_profile, cls.lab)
+        cls.role = instantiate_opnfvrole()
+        cls.pubnet = instantiate_publicnet(10, cls.lab)
+
+        cls.lab_selected = 'lab_' + str(cls.lab.lab_user.id) + '_selected'
+        cls.host_selected = 'host_' + str(cls.host_profile.id) + '_selected'
+
+        cls.post_data = cls.build_post_data()
+
+        cls.client = Client()
+
+    @classmethod
+    def build_post_data(cls):
+        post_data = {}
+        post_data['filter_field'] = '{"hosts":[{"host_' + str(cls.host_profile.id) + '":"true"}], "labs": [{"lab_' + str(cls.lab.lab_user.id) + '":"true"}]}'
+        post_data['purpose'] = 'purposefieldcontentstring'
+        post_data['project'] = 'projectfieldcontentstring'
+        post_data['length'] = '3'
+        post_data['ignore_this'] = 1
+        post_data['users'] = ''
+        post_data['hostname'] = 'hostnamefieldcontentstring'
+        post_data['image'] = str(cls.image.id)
+        post_data['installer'] = str(cls.installer.id)
+        post_data['scenario'] = str(cls.scenario.id)
+        return post_data
+
+    def post(self, changed_fields={}):
+        payload = self.post_data.copy()
+        payload.update(changed_fields)
+        response = self.client.post('/booking/quick/', payload)
+        return response
+
+    def setUp(self):
+        self.client.login(
+            username=self.loginuser.username, password="testpassword")
+
+    def is_valid_booking(self, booking):
+        self.assertEqual(booking.owner, self.loginuser)
+        self.assertEqual(booking.purpose, 'purposefieldcontentstring')
+        self.assertEqual(booking.project, 'projectfieldcontentstring')
+        delta = booking.end - booking.start
+        delta -= datetime.timedelta(days=3)
+        self.assertLess(delta, datetime.timedelta(minutes=1))
+
+        resourcebundle = booking.resource
+        configbundle = booking.config_bundle
+
+        self.assertEqual(self.installer, configbundle.opnfv_config.first().installer)
+        self.assertEqual(self.scenario, configbundle.opnfv_config.first().scenario)
+        self.assertEqual(resourcebundle.template.getHosts()[0].profile, self.host_profile)
+        self.assertEqual(resourcebundle.template.getHosts()[0].resource.name, 'hostnamefieldcontentstring')
+
+        return True
+
+    def test_with_too_long_length(self):
+        response = self.post({'length': '22'})
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIsNone(Booking.objects.first())
+
+    def test_with_negative_length(self):
+        response = self.post({'length': '-1'})
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIsNone(Booking.objects.first())
+
+    def test_with_invalid_installer(self):
+        response = self.post({'installer': str(self.installer.id + 100)})
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIsNone(Booking.objects.first())
+
+    def test_with_invalid_scenario(self):
+        response = self.post({'scenario': str(self.scenario.id + 100)})
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIsNone(Booking.objects.first())
+
+    def test_with_invalid_host_id(self):
+        response = self.post({'filter_field': '{"hosts":[{"host_' + str(self.host_profile.id + 100) + '":"true"}], "labs": [{"lab_' + str(self.lab.lab_user.id) + '":"true"}]}'})
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIsNone(Booking.objects.first())
+
+    def test_with_invalid_lab_id(self):
+        response = self.post({'filter_field': '{"hosts":[{"host_' + str(self.host_profile.id) + '":"true"}], "labs": [{"lab_' + str(self.lab.lab_user.id + 100) + '":"true"}]}'})
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIsNone(Booking.objects.first())
+
+    def test_with_invalid_empty_filter_field(self):
+        response = self.post({'filter_field': ''})
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIsNone(Booking.objects.first())
+
+    def test_with_garbage_users_field(self):  # expected behavior: treat as though field is empty if it has garbage data
+        response = self.post({'users': 'X�]QP�槰DP�+m���h�U�_�yJA:.rDi��QN|.��C��n�P��F!��D�����5È…j�9�LV��'})  # output from /dev/urandom
+
+        self.assertEqual(response.status_code, 200)
+        booking = Booking.objects.first()
+        self.assertIsNotNone(booking)
+        self.assertTrue(self.is_valid_booking(booking))
+
+    def test_with_valid_form(self):
+        response = self.post()
+
+        self.assertEqual(response.status_code, 200)
+        booking = Booking.objects.first()
+        self.assertIsNotNone(booking)
+        self.assertTrue(self.is_valid_booking(booking))
index 9c16a06..7111bf8 100644 (file)
@@ -50,3 +50,7 @@ class InvalidVlanConfigurationException(Exception):
 
 class NetworkExistsException(Exception):
     pass
+
+
+class BookingLengthException(Exception):
+    pass
diff --git a/src/dashboard/testing_utils.py b/src/dashboard/testing_utils.py
new file mode 100644 (file)
index 0000000..e98b5e6
--- /dev/null
@@ -0,0 +1,324 @@
+##############################################################################
+# Copyright (c) 2018 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 django.contrib.auth.models import User
+
+import json
+
+from account.models import UserProfile, Lab, LabStatus, VlanManager, PublicNetwork
+from resource_inventory.models import (
+    Host,
+    HostProfile,
+    InterfaceProfile,
+    DiskProfile,
+    CpuProfile,
+    Opsys,
+    Image,
+    Scenario,
+    Installer,
+    OPNFVRole,
+    RamProfile,
+)
+
+
+class BookingContextData(object):
+    def prepopulate(self, *args, **kwargs):
+        self.loginuser = instantiate_user(False, username=kwargs.get("login_username", "newtestuser"), password="testpassword")
+        instantiate_userprofile(self.loginuser, True)
+
+        lab_user = kwargs.get("lab_user", instantiate_user(True))
+        self.lab = instantiate_lab(lab_user)
+
+        self.host_profile = make_hostprofile_set(self.lab)
+        self.scenario = instantiate_scenario()
+        self.installer = instantiate_installer([self.scenario])
+        os = instantiate_os([self.installer])
+        self.image = instantiate_image(self.lab, 1, self.loginuser, os, self.host_profile)
+        self.host = instantiate_host(self.host_profile, self.lab)
+        self.role = instantiate_opnfvrole()
+        self.pubnet = instantiate_publicnet(10, self.lab)
+
+
+def instantiate_user(is_superuser,
+                     username="testuser",
+                     password="testpassword",
+                     email="default_email@user.com"
+                     ):
+    user = User.objects.create_user(username=username, email=email, password=password)
+    user.is_superuser = is_superuser
+
+    user.save()
+
+    return user
+
+
+def instantiate_userprofile(user=None, can_book_multiple=False):
+    if not user:
+        user = instantiate_user(True, 'test_user', 'test_pass', 'test_user@test_site.org')
+    userprofile = UserProfile()
+    userprofile.user = user
+    userprofile.booking_privledge = can_book_multiple
+
+    userprofile.save()
+
+    return user
+
+
+def instantiate_vlanmanager(vlans=None,
+                            block_size=20,
+                            allow_overlapping=False,
+                            reserved_vlans=None
+                            ):
+    vlanmanager = VlanManager()
+    if not vlans:
+        vlans = []
+        for vlan in range(0, 4095):
+            vlans.append(vlan % 2)
+    vlanmanager.vlans = json.dumps(vlans)
+    if not reserved_vlans:
+        reserved_vlans = []
+        for vlan in range(0, 4095):
+            reserved_vlans.append(0)
+    vlanmanager.reserved_vlans = json.dumps(vlans)
+    vlanmanager.block_size = block_size
+    vlanmanager.allow_overlapping = allow_overlapping
+
+    vlanmanager.save()
+
+    return vlanmanager
+
+
+def instantiate_lab(user=None,
+                    name="Test Lab Instance",
+                    status=LabStatus.UP,
+                    vlan_manager=None
+                    ):
+    if not vlan_manager:
+        vlan_manager = instantiate_vlanmanager()
+
+    if not user:
+        user = instantiate_user(True, 'test_user', 'test_pass', 'test_user@test_site.org')
+
+    lab = Lab()
+    lab.lab_user = user
+    lab.name = name
+    lab.contact_email = 'test_lab@test_site.org'
+    lab.contact_phone = '603 123 4567'
+    lab.status = status
+    lab.vlan_manager = vlan_manager
+    lab.description = 'test lab instantiation'
+    lab.api_token = '12345678'
+
+    lab.save()
+
+    return lab
+
+
+"""
+resource_inventory instantiation section for permenant resources
+"""
+
+
+def make_hostprofile_set(lab, name="test_hostprofile"):
+    hostprof = instantiate_hostprofile(lab, name=name)
+    instantiate_diskprofile(hostprof, 500, name=name)
+    instantiate_cpuprofile(hostprof)
+    instantiate_interfaceprofile(hostprof, name=name)
+    instantiate_ramprofile(hostprof)
+
+    return hostprof
+
+
+def instantiate_hostprofile(lab,
+                            host_type=0,
+                            name="test hostprofile instance"
+                            ):
+    hostprof = HostProfile()
+    hostprof.host_type = host_type
+    hostprof.name = name
+    hostprof.description = 'test hostprofile instance'
+    hostprof.save()
+    hostprof.labs.add(lab)
+
+    hostprof.save()
+
+    return hostprof
+
+
+def instantiate_ramprofile(host,
+                           channels=4,
+                           amount=256):
+    ramprof = RamProfile()
+    ramprof.host = host
+    ramprof.amount = amount
+    ramprof.channels = channels
+    ramprof.save()
+
+    return ramprof
+
+
+def instantiate_diskprofile(hostprofile,
+                            size=0,
+                            media_type="SSD",
+                            name="test diskprofile",
+                            rotation=0,
+                            interface="sata"):
+
+    diskprof = DiskProfile()
+    diskprof.name = name
+    diskprof.size = size
+    diskprof.media_type = media_type
+    diskprof.host = hostprofile
+    diskprof.rotation = rotation
+    diskprof.interface = interface
+
+    diskprof.save()
+
+    return diskprof
+
+
+def instantiate_cpuprofile(hostprofile,
+                           cores=4,
+                           architecture="x86_64",
+                           cpus=4,
+                           ):
+    cpuprof = CpuProfile()
+    cpuprof.cores = cores
+    cpuprof.architecture = architecture
+    cpuprof.cpus = cpus
+    cpuprof.host = hostprofile
+    cpuprof.cflags = ''
+
+    cpuprof.save()
+
+    return cpuprof
+
+
+def instantiate_interfaceprofile(hostprofile,
+                                 speed=1000,
+                                 name="test interface profile",
+                                 nic_type="pcie"
+                                 ):
+    intprof = InterfaceProfile()
+    intprof.host = hostprofile
+    intprof.name = name
+    intprof.speed = speed
+    intprof.nic_type = nic_type
+
+    intprof.save()
+
+    return intprof
+
+
+def instantiate_image(lab,
+                      lab_id,
+                      owner,
+                      os,
+                      host_profile,
+                      public=True,
+                      name="default image",
+                      description="default image"
+                      ):
+    image = Image()
+    image.from_lab = lab
+    image.lab_id = lab_id
+    image.os = os
+    image.host_type = host_profile
+    image.public = public
+    image.name = name
+    image.description = description
+
+    image.save()
+
+    return image
+
+
+def instantiate_scenario(name="test scenario"):
+    scenario = Scenario()
+    scenario.name = name
+    scenario.save()
+    return scenario
+
+
+def instantiate_installer(supported_scenarios,
+                          name="test installer"
+                          ):
+    installer = Installer()
+    installer.name = name
+    installer.save()
+    for scenario in supported_scenarios:
+        installer.sup_scenarios.add(scenario)
+
+    installer.save()
+    return installer
+
+
+def instantiate_os(supported_installers,
+                   name="test operating system",
+                   ):
+    os = Opsys()
+    os.name = name
+    os.save()
+    for installer in supported_installers:
+        os.sup_installers.add(installer)
+    os.save()
+    return os
+
+
+def instantiate_host(host_profile,
+                     lab,
+                     labid="test_host",
+                     name="test_host",
+                     booked=False,
+                     working=True,
+                     config=None,
+                     template=None,
+                     bundle=None,
+                     model="Model 1",
+                     vendor="ACME"):
+    host = Host()
+    host.lab = lab
+    host.profile = host_profile
+    host.name = name
+    host.booked = booked
+    host.working = working
+    host.config = config
+    host.template = template
+    host.bundle = bundle
+    host.model = model
+    host.vendor = vendor
+
+    host.save()
+
+    return host
+
+
+def instantiate_opnfvrole(name="Jumphost",
+                          description="test opnfvrole"):
+    role = OPNFVRole()
+    role.name = name
+    role.description = description
+    role.save()
+
+    return role
+
+
+def instantiate_publicnet(vlan,
+                          lab,
+                          in_use=False,
+                          cidr="0.0.0.0/0",
+                          gateway="0.0.0.0"):
+    pubnet = PublicNetwork()
+    pubnet.lab = lab
+    pubnet.vlan = vlan
+    pubnet.cidr = cidr
+    pubnet.gateway = gateway
+    pubnet.save()
+
+    return pubnet
index f03c2cc..240cf85 100644 (file)
@@ -18,13 +18,13 @@ class NotificationHandler(object):
     @classmethod
     def notify_new_booking(cls, booking):
         template = "notifier/new_booking.html"
-        titles = ["You have a new Booking", "You have been added to a Booking"]
+        titles = ["You have a new booking (" + str(booking.id) + ")", "You have been added to a booking (" + str(booking.id) + ")"]
         cls.booking_notify(booking, template, titles)
 
     @classmethod
     def notify_booking_end(cls, booking):
         template = "notifier/end_booking.html"
-        titles = ["Your booking has ended", "A booking you collaborate on has ended"]
+        titles = ["Your booking (" + str(booking.id) + ") has ended", "A booking (" + str(booking.id) + ") that you collaborate on has ended"]
         cls.booking_notify(booking, template, titles)
 
     @classmethod
diff --git a/src/notifier/migrations/0003_auto_20190123_1741.py b/src/notifier/migrations/0003_auto_20190123_1741.py
new file mode 100644 (file)
index 0000000..f491993
--- /dev/null
@@ -0,0 +1,23 @@
+# Generated by Django 2.1 on 2019-01-23 17:41
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('notifier', '0002_auto_20181102_1631'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='notification',
+            name='is_html',
+            field=models.BooleanField(default=True),
+        ),
+        migrations.AddField(
+            model_name='notification',
+            name='is_read',
+            field=models.BooleanField(default=True),
+        ),
+    ]
diff --git a/src/notifier/migrations/0004_auto_20190124_2115.py b/src/notifier/migrations/0004_auto_20190124_2115.py
new file mode 100644 (file)
index 0000000..306ec7b
--- /dev/null
@@ -0,0 +1,23 @@
+# Generated by Django 2.1 on 2019-01-24 21:15
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('account', '0003_publicnetwork'),
+        ('notifier', '0003_auto_20190123_1741'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='notification',
+            name='is_read',
+        ),
+        migrations.AddField(
+            model_name='notification',
+            name='read_by',
+            field=models.ManyToManyField(related_name='read_notifications', to='account.UserProfile'),
+        ),
+    ]
diff --git a/src/notifier/migrations/0005_auto_20190306_1616.py b/src/notifier/migrations/0005_auto_20190306_1616.py
new file mode 100644 (file)
index 0000000..d92c988
--- /dev/null
@@ -0,0 +1,18 @@
+# Generated by Django 2.1 on 2019-03-06 16:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('notifier', '0004_auto_20190124_2115'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='notification',
+            name='recipients',
+            field=models.ManyToManyField(related_name='notifications', to='account.UserProfile'),
+        ),
+    ]
index 5e7c60e..49189e8 100644 (file)
@@ -14,7 +14,9 @@ from account.models import UserProfile
 class Notification(models.Model):
     title = models.CharField(max_length=150)
     content = models.TextField()
-    recipients = models.ManyToManyField(UserProfile)
+    recipients = models.ManyToManyField(UserProfile, related_name='notifications')
+    is_html = models.BooleanField(default=True)
+    read_by = models.ManyToManyField(UserProfile, related_name='read_notifications')
 
     def __str__(self):
         return self.title
index 4ee757f..3a85eda 100644 (file)
@@ -7,27 +7,52 @@
 # http://www.apache.org/licenses/LICENSE-2.0
 ##############################################################################
 
-from notifier.models import Notification
 from django.shortcuts import render
+from notifier.models import Notification
+from django.db.models import Q
 
 
 def InboxView(request):
     if request.user.is_authenticated:
         user = request.user
     else:
-        return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
+        return render(request, "dashboard/login.html",
+                      {'title': 'Authentication Required'})
 
-    return render(request, "notifier/inbox.html", {'notifications': Notification.objects.filter(recipients=user.userprofile)})
+    return render(request,
+                  "notifier/inbox.html",
+                  {'unread_notifications': Notification.objects.filter(recipients=user.userprofile).order_by('-id').filter(~Q(read_by=user.userprofile)),
+                      'read_notifications': Notification.objects.filter(recipients=user.userprofile).order_by('-id').filter(read_by=user.userprofile)})
 
 
 def NotificationView(request, notification_id):
+
     if request.user.is_authenticated:
         user = request.user
     else:
-        return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
+        return render(request,
+                      "dashboard/login.html",
+                      {'title': 'Authentication Required'})
 
     notification = Notification.objects.get(id=notification_id)
     if user.userprofile not in notification.recipients.all():
-        return render(request, "dashboard/login.html", {'title': 'Access Denied'})
-
-    return render(request, "notifier/notification.html", {'notification': notification})
+        return render(request,
+                      "dashboard/login.html", {'title': 'Access Denied'})
+
+    notification.read_by.add(user.userprofile)
+    notification.save()
+    if request.method == 'POST':
+        if 'delete' in request.POST:
+            # handle deleting
+            notification.recipients.remove(user.userprofile)
+            if not notification.recipients.exists():
+                notification.delete()
+            else:
+                notification.save()
+
+        if 'unread' in request.POST:
+            notification.read_by.remove(user.userprofile)
+            notification.save()
+
+    return render(request,
+                  "notifier/notification.html", {'notification': notification})
diff --git a/src/resource_inventory/migrations/0007_auto_20190306_1616.py b/src/resource_inventory/migrations/0007_auto_20190306_1616.py
new file mode 100644 (file)
index 0000000..19a49c5
--- /dev/null
@@ -0,0 +1,31 @@
+# Generated by Django 2.1 on 2019-03-06 16:16
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('resource_inventory', '0006_auto_20190124_1700'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='RemoteInfo',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('address', models.CharField(max_length=15)),
+                ('mac_address', models.CharField(max_length=17)),
+                ('password', models.CharField(max_length=100)),
+                ('user', models.CharField(max_length=100)),
+                ('management_type', models.CharField(default='ipmi', max_length=50)),
+                ('versions', models.CharField(max_length=100)),
+            ],
+        ),
+        migrations.AlterField(
+            model_name='genericinterface',
+            name='profile',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.InterfaceProfile'),
+        ),
+    ]
diff --git a/src/resource_inventory/migrations/0008_host_remote_management.py b/src/resource_inventory/migrations/0008_host_remote_management.py
new file mode 100644 (file)
index 0000000..f74a535
--- /dev/null
@@ -0,0 +1,19 @@
+# Generated by Django 2.1 on 2019-03-06 16:42
+
+from django.db import migrations, models
+import resource_inventory.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('resource_inventory', '0007_auto_20190306_1616'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='host',
+            name='remote_management',
+            field=models.ForeignKey(default=resource_inventory.models.get_default_remote_info, on_delete=models.SET(resource_inventory.models.get_default_remote_info), to='resource_inventory.RemoteInfo'),
+        ),
+    ]
index ebf63cc..4e3974e 100644 (file)
@@ -291,6 +291,26 @@ class HostConfiguration(models.Model):
         return "config with " + str(self.host) + " and image " + str(self.image)
 
 
+class RemoteInfo(models.Model):
+    address = models.CharField(max_length=15)
+    mac_address = models.CharField(max_length=17)
+    password = models.CharField(max_length=100)
+    user = models.CharField(max_length=100)
+    management_type = models.CharField(max_length=50, default="ipmi")
+    versions = models.CharField(max_length=100)  # json serialized list of floats
+
+
+def get_default_remote_info():
+    return RemoteInfo.objects.get_or_create(
+        address="default",
+        mac_address="default",
+        password="default",
+        user="default",
+        management_type="default",
+        versions="[default]"
+    )[0].pk
+
+
 # Concrete host, actual machine in a lab
 class Host(models.Model):
     id = models.AutoField(primary_key=True)
@@ -305,6 +325,7 @@ class Host(models.Model):
     working = models.BooleanField(default=True)
     vendor = models.CharField(max_length=100, default="unknown")
     model = models.CharField(max_length=150, default="unknown")
+    remote_management = models.ForeignKey(RemoteInfo, default=get_default_remote_info, on_delete=models.SET(get_default_remote_info))
 
     def __str__(self):
         return self.name
diff --git a/src/resource_inventory/pdf_templater.py b/src/resource_inventory/pdf_templater.py
new file mode 100644 (file)
index 0000000..9f7e7f1
--- /dev/null
@@ -0,0 +1,173 @@
+##############################################################################
+# Copyright (c) 2018 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 django.template.loader import render_to_string
+import booking
+from resource_inventory.models import Host, InterfaceProfile
+
+
+class PDFTemplater:
+    """
+    Utility class to create a full PDF yaml file
+    """
+
+    @classmethod
+    def makePDF(cls, resource):
+        """
+        fills the pod descriptor file template with info about the resource
+        """
+        template = "dashboard/pdf.yaml"
+        info = {}
+        info['details'] = cls.get_pdf_details(resource)
+        info['jumphost'] = cls.get_pdf_jumphost(resource)
+        info['nodes'] = cls.get_pdf_nodes(resource)
+
+        return render_to_string(template, context=info)
+
+    @classmethod
+    def get_pdf_details(cls, resource):
+        """
+        Info for the "details" section
+        """
+        details = {}
+        owner = "Anon"
+        email = "email@mail.com"
+        resource_lab = resource.template.lab
+        lab = resource_lab.name
+        location = resource_lab.location
+        pod_type = "development"
+        link = "https://wiki.opnfv.org/display/INF/Pharos+Laas"
+
+        try:
+            # try to get more specific info that may fail, we dont care if it does
+            booking_owner = booking.models.Booking.objects.get(resource=resource).owner
+            owner = booking_owner.username
+            email = booking_owner.userprofile.email_addr
+        except Exception:
+            pass
+
+        details['contact'] = email
+        details['lab'] = lab
+        details['link'] = link
+        details['owner'] = owner
+        details['location'] = location
+        details['type'] = pod_type
+
+        return details
+
+    @classmethod
+    def get_pdf_jumphost(cls, resource):
+        """
+        returns a dict of all the info for the "jumphost" section
+        """
+        jumphost = Host.objects.get(bundle=resource, config__opnfvRole__name__iexact="jumphost")
+        jumphost_info = cls.get_pdf_host(jumphost)
+        remote_params = jumphost_info['remote_management']  # jumphost has extra block not in normal hosts
+        remote_params.pop("address")
+        remote_params.pop("mac_address")
+        jumphost_info['remote_params'] = remote_params
+        jumphost_info['os'] = jumphost.config.image.os.name
+        return jumphost_info
+
+    @classmethod
+    def get_pdf_nodes(cls, resource):
+        """
+        returns a list of all the "nodes" (every host except jumphost)
+        """
+        pdf_nodes = []
+        nodes = Host.objects.filter(bundle=resource).exclude(config__opnfvRole__name__iexact="jumphost")
+        for node in nodes:
+            pdf_nodes.append(cls.get_pdf_host(node))
+
+        return pdf_nodes
+
+    @classmethod
+    def get_pdf_host(cls, host):
+        """
+        method to gather all needed info about a host
+        returns a dict
+        """
+        host_info = {}
+        host_info['name'] = host.template.resource.name
+        host_info['node'] = cls.get_pdf_host_node(host)
+        host_info['disks'] = []
+        for disk in host.profile.storageprofile.all():
+            host_info['disks'].append(cls.get_pdf_host_disk(disk))
+
+        host_info['interfaces'] = []
+        for interface in host.interfaces.all():
+            host_info['interfaces'].append(cls.get_pdf_host_iface(interface))
+
+        host_info['remote_management'] = cls.get_pdf_host_remote_management(host)
+
+        return host_info
+
+    @classmethod
+    def get_pdf_host_node(cls, host):
+        """
+        returns "node" info for a given host
+        """
+        d = {}
+        d['type'] = "baremetal"
+        d['vendor'] = host.vendor
+        d['model'] = host.model
+        d['memory'] = str(host.profile.ramprofile.first().amount) + "G"
+
+        cpu = host.profile.cpuprofile.first()
+        d['arch'] = cpu.architecture
+        d['cpus'] = cpu.cpus
+        d['cores'] = cpu.cores
+        cflags = cpu.cflags
+        if cflags and cflags.strip():
+            d['cpu_cflags'] = cflags
+        else:
+            d['cpu_cflags'] = "none"
+
+        return d
+
+    @classmethod
+    def get_pdf_host_disk(cls, disk):
+        """
+        returns a dict describing the given disk
+        """
+        disk_info = {}
+        disk_info['name'] = disk.name
+        disk_info['capacity'] = str(disk.size) + "G"
+        disk_info['type'] = disk.media_type
+        disk_info['interface'] = disk.interface
+        disk_info['rotation'] = disk.rotation
+        return disk_info
+
+    @classmethod
+    def get_pdf_host_iface(cls, interface):
+        """
+        returns a dict describing given interface
+        """
+        iface_info = {}
+        iface_info['features'] = "none"
+        iface_info['mac_address'] = interface.mac_address
+        iface_info['name'] = interface.name
+        profile = InterfaceProfile.objects.get(host=interface.host.profile, name=interface.name)
+        iface_info['speed'] = str(int(profile.speed / 1000)) + "gb"
+        return iface_info
+
+    @classmethod
+    def get_pdf_host_remote_management(cls, host):
+        """
+        gives the remote params of the host
+        """
+        mgmt = {}
+        mgmt['address'] = "I dunno"
+        mgmt['mac_address'] = "I dunno"
+        mgmt['pass'] = "I dunno"
+        mgmt['type'] = "I dunno"
+        mgmt['user'] = "I dunno"
+        mgmt['versions'] = ["I dunno"]
+        return mgmt
index 812fcd7..52b0055 100644 (file)
@@ -8,9 +8,6 @@
 ##############################################################################
 
 
-from django.template.loader import render_to_string
-
-import booking
 from dashboard.exceptions import (
     ResourceExistenceException,
     ResourceAvailabilityException,
@@ -38,6 +35,31 @@ class ResourceManager:
         hostprofileset = HostProfile.objects.filter(host__in=hostset, labs=lab)
         return set(hostprofileset)
 
+    def hostsAvailable(self, grb):
+        """
+        This method will check if the given GenericResourceBundle
+        is available. No changes to the database
+        """
+
+        # count up hosts
+        profile_count = {}
+        for host in grb.getHosts():
+            if host.profile not in profile_count:
+                profile_count[host.profile] = 0
+            profile_count[host.profile] += 1
+
+        # check that all required hosts are available
+        for profile in profile_count.keys():
+            available = Host.objects.filter(
+                booked=False,
+                lab=grb.lab,
+                profile=profile
+            ).count()
+            needed = profile_count[profile]
+            if available < needed:
+                return False
+        return True
+
     # public interface
     def deleteResourceBundle(self, resourceBundle):
         for host in Host.objects.filter(bundle=resourceBundle):
@@ -117,90 +139,3 @@ class ResourceManager:
     def fail_acquire(self, hosts):
         for host in hosts:
             self.releaseHost(host)
-
-    def makePDF(self, resource):
-        """
-        fills the pod descriptor file template with info about the resource
-        """
-        template = "dashboard/pdf.yaml"
-        info = {}
-        info['details'] = self.get_pdf_details(resource)
-        info['jumphost'] = self.get_pdf_jumphost(resource)
-        info['nodes'] = self.get_pdf_nodes(resource)
-
-        return render_to_string(template, context=info)
-
-    def get_pdf_details(self, resource):
-        details = {}
-        owner = "Anon"
-        email = "email@mail.com"
-        resource_lab = resource.template.lab
-        lab = resource_lab.name
-        location = resource_lab.location
-        pod_type = "development"
-        link = "https://wiki.opnfv.org/display/INF/Pharos+Laas"
-
-        try:
-            # try to get more specific info that may fail, we dont care if it does
-            booking_owner = booking.models.Booking.objects.get(resource=resource).owner
-            owner = booking_owner.username
-            email = booking_owner.userprofile.email_addr
-        except Exception:
-            pass
-
-        details['owner'] = owner
-        details['email'] = email
-        details['lab'] = lab
-        details['location'] = location
-        details['type'] = pod_type
-        details['link'] = link
-
-        return details
-
-    def get_pdf_jumphost(self, resource):
-        jumphost = Host.objects.get(bundle=resource, config__opnfvRole__name__iexact="jumphost")
-        return self.get_pdf_host(jumphost)
-
-    def get_pdf_nodes(self, resource):
-        pdf_nodes = []
-        nodes = Host.objects.filter(bundle=resource).exclude(config__opnfvRole__name__iexact="jumphost")
-        for node in nodes:
-            pdf_nodes.append(self.get_pdf_host(node))
-
-        return pdf_nodes
-
-    def get_pdf_host(self, host):
-        host_info = {}
-        host_info['name'] = host.template.resource.name
-        host_info['node'] = {}
-        host_info['node']['type'] = "baremetal"
-        host_info['node']['vendor'] = host.vendor
-        host_info['node']['model'] = host.model
-        host_info['node']['arch'] = host.profile.cpuprofile.first().architecture
-        host_info['node']['cpus'] = host.profile.cpuprofile.first().cpus
-        host_info['node']['cores'] = host.profile.cpuprofile.first().cores
-        cflags = host.profile.cpuprofile.first().cflags
-        if cflags and cflags.strip():
-            host_info['node']['cpu_cflags'] = cflags
-        host_info['node']['memory'] = str(host.profile.ramprofile.first().amount) + "G"
-        host_info['disks'] = []
-        for disk in host.profile.storageprofile.all():
-            disk_info = {}
-            disk_info['name'] = disk.name
-            disk_info['capacity'] = str(disk.size) + "G"
-            disk_info['type'] = disk.media_type
-            disk_info['interface'] = disk.interface
-            disk_info['rotation'] = disk.rotation
-            host_info['disks'].append(disk_info)
-
-        host_info['interfaces'] = []
-        for interface in host.interfaces.all():
-            iface_info = {}
-            iface_info['name'] = interface.name
-            iface_info['address'] = "unknown"
-            iface_info['mac_address'] = interface.mac_address
-            vlans = "|".join([str(vlan.vlan_id) for vlan in interface.config.all()])
-            iface_info['vlans'] = vlans
-            host_info['interfaces'].append(iface_info)
-
-        return host_info
diff --git a/src/templates/dashboard/idf.yaml b/src/templates/dashboard/idf.yaml
new file mode 100644 (file)
index 0000000..5da20c4
--- /dev/null
@@ -0,0 +1,52 @@
+idf:
+  version: {{version|default:"0.1"}}
+  net_config:
+    oob:
+      ip-range: {{net_config.oob.ip-range}}
+      vlan: {{net_config.oob.vlan}}
+    admin:
+      interface: {{net_config.admin.interface}}
+      vlan: {{net_config.admin.vlan}}
+      network: {{net_config.admin.network}}
+      mask: {{net_config.admin.mask}}
+    mgmt:
+      interface: {{net_config.mgmt.interface}}
+      vlan: {{net_config.mgmt.vlan}}
+      network: {{net_config.mgmt.network}}
+      mask: {{net_config.mgmt.mask}}
+    private:
+      interface: {{net_config.private.interface}}
+      vlan: {{net_config.private.vlan}}
+      network: {{net_config.private.network}}
+      mask: {{net_config.private.mask}}
+    public:
+      interface: {{net_config.public.interface}}
+      vlan: {{net_config.public.vlan}}
+      network: {{net_config.public.network}}
+      mask: {{net_config.public.mask}}
+      ip-range: {{net_config.public.ip-range}}
+      mask: {{net_config.public.mask}}
+      gateway: {{net_config.public.gateway}}
+      dns:
+      {% for serv in net_config.public.dns %}
+      - {{serv}}
+      {% endfor %}
+  fuel:
+    jumphost:
+      bridges:
+        admin: {{fuel.jumphost.bridges.admin}}
+        mgmt: {{fuel.jumphost.bridges.mgmt}}
+        private: {{fuel.jumphost.bridges.private}}
+        public: {{fuel.jumphost.bridges.public}}
+    network:
+      {% for node in fuel.network.nodes %}
+      node:
+      - interfaces:
+        {% for iface in node.interfaces %}
+        - {{ iface }}
+        {% endfor %}
+      - busaddr:
+        {% for addr in node.bus_addrs %}
+        - {{addr}}
+        {% endfor %}
+      {% endfor %}
index 297e04b..c893919 100644 (file)
@@ -1,95 +1,92 @@
 ---
 version: {{version|default:"1.0"}}
 details:
-    pod_owner: {{details.owner}}
-    contact: {{details.contact}}
-    lab: {{details.lab}}
-    location: {{details.location}}
-    type: {{details.type}}
-    link: {{details.link}}
-
+  contact: {{details.contact}}
+  lab: {{details.lab}}
+  link: {{details.link}}
+  location: {{details.location}}
+  pod_owner: {{details.owner}}
+  type: {{details.type}}
 jumphost:
-    name: {{jumphost.name}}
-    node:
-        type: {{jumphost.node.type}}
-        vendor: {{jumphost.node.vendor}}
-        model: {{jumphost.node.model}}
-        arch: {{jumphost.node.arch}}
-        cpus: {{jumphost.node.cpus}}
-        cpu_cflags: {{jumphost.node.cpu_cflags}}
-        cores: {{jumphost.node.cores}}
-        memory: {{jumphost.node.memory}}
-    disks:
-        {% for disk in jumphost.disks %}
-          - name: {{disk.name}}
-            disk_capacity: {{disk.capacity}}
-            disk_type: {{disk.type}}
-            disk_interface: {{disk.interface}}
-            disk_rotation: {{disk.rotation}}
-
-        {% endfor %}
-    os: {{jumphost.os}}
-    remote_params:
-        type: {{jumphost.remote.type}}
-        versions:
-            {% for version in jumphost.remote.versions %}
-          - {{version}}
-            {% endfor %}
-        user: {{jumphost.remote.user}}
-        pass: {{jumphost.remote.pass}}
-    remote_management:
-        type: {{jumphost.remote.type}}
-        versions:
-            {% for version in jumphost.remote.versions %}
-          - {{version}}
-            {% endfor %}
-        user: {{jumphost.remote.user}}
-        pass: {{jumphost.remote.pass}}
-        address: {{jumphost.remote.address}}
-        mac_address: {{jumphost.remote.mac_address}}
-    interfaces:
-        {% for interface in jumphost.interfaces %}
-      - name: {{interface.name}}
-        address: {{interface.address}}
-        mac_address: {{interface.mac_address}}
-        vlan: {{interface.vlan}}
-        {% endfor %}
+  disks:
+  {% for disk in jumphost.disks %}
+  - disk_capacity: {{disk.capacity}}
+    disk_interface: {{disk.interface}}
+    disk_rotation: {{disk.rotation}}
+    disk_type: {{disk.type}}
+    name: {{disk.name}}
+  {% endfor %}
+  interfaces:
+    {% for interface in jumphost.interfaces %}
+  - features: {{interface.features}}
+    mac_address: {{interface.mac_address}}
+    name: {{interface.name}}
+    speed: {{interface.speed}}
+    {% endfor %}
+  name: {{jumphost.name}}
+  node:
+    arch: {{jumphost.node.arch}}
+    cores: {{jumphost.node.cores}}
+    cpu_cflags: {{jumphost.node.cpu_cflags}}
+    cpus: {{jumphost.node.cpus}}
+    memory: {{jumphost.node.memory}}
+    model: {{jumphost.node.model}}
+    type: {{jumphost.node.type}}
+    vendor: {{jumphost.node.vendor}}
+  os: {{jumphost.os}}
+  remote_management:
+    address: {{jumphost.remote.address}}
+    mac_address: {{jumphost.remote.mac_address}}
+    pass: {{jumphost.remote.pass}}
+    type: {{jumphost.remote.type}}
+    user: {{jumphost.remote.user}}
+    versions:
+      {% for version in jumphost.remote.versions %}
+    - {{version}}
+      {% endfor %}
+  remote_params:
+    pass: {{jumphost.remote.pass}}
+    type: {{jumphost.remote.type}}
+    user: {{jumphost.remote.user}}
+    versions:
+      {% for version in jumphost.remote.versions %}
+    - {{version}}
+      {% endfor %}
 nodes:
-    {% for node in nodes %}
-  - name: {{node.name}}
-    node:
-        type: {{node.node.type}}
-        vendor: {{node.node.vendor}}
-        model: {{node.node.model}}
-        arch: {{node.node.arch}}
-        cpus: {{node.node.cpus}}
-        cpu_cflags: {{node.node.cpu_cflags}}
-        cores: {{node.node.cores}}
-        memory: {{node.node.memory}}
-    disks:
-        {% for disk in node.disks %}
-      - name: {{disk.name}}
-        disk_capacity: {{disk.capacity}}
-        disk_type: {{disk.type}}
-        disk_interface: {{disk.interface}}
-        disk_rotation: {{disk.rotation}}
-
-        {% endfor %}
-    remote_management:
-        type: {{node.remote.type}}
-        versions:
-            {% for version in node.remote.versions %}
-          - {{version}}
-            {% endfor %}
-        user: {{node.remote.user}}
-        pass: {{node.remote.pass}}
-        address: {{node.remote.address}}
-        mac_address: {{node.remote.mac_address}}
-    interfaces:
-        {% for interface in node.interfaces %}
-      - name: {{interface.name}}
-        address: {{interface.address}}
-        mac_address: {{interface.mac_address}}
-        vlan: {{interface.vlan}}
-        {% endfor %}
+{% for node in nodes %}
+- disks:
+    {% for disk in node.disks %}
+  - disk_capacity: {{disk.capacity}}
+    disk_interface: {{disk.interface}}
+    disk_rotation: {{disk.rotation}}
+    disk_type: {{disk.type}}
+    name: {{disk.name}}
+    {% endfor %}
+  interfaces:
+    {% for interface in node.interfaces %}
+  - features: {{interface.features}}
+    mac_address: {{interface.mac_address}}
+    name: {{interface.name}}
+    speed: {{interface.speed}}
     {% endfor %}
+  name: {{node.name}}
+  node:
+    arch: {{node.node.arch}}
+    cores: {{node.node.cores}}
+    cpu_cflags: {{node.node.cpu_cflags}}
+    cpus: {{node.node.cpus}}
+    memory: {{node.node.memory}}
+    model: {{node.node.model}}
+    type: {{node.node.type}}
+    vendor: {{node.node.vendor}}
+  remote_management:
+    address: {{node.remote.address}}
+    mac_address: {{node.remote.mac_address}}
+    pass: {{node.remote.pass}}
+    type: {{node.remote.type}}
+    user: {{node.remote.user}}
+    versions:
+      {% for version in node.remote.versions %}
+    - {{version}}
+      {% endfor %}
+{% endfor %}
index 471eae4..4184d1d 100644 (file)
@@ -9,7 +9,7 @@
 
   .inbox-panel {
     display: grid;
-    grid-template-columns: 30% 70%;
+    grid-template-columns: 30% 5% 65%;
   }
 
   .section-panel {
@@ -22,7 +22,8 @@
   }
 
   .card-container {
-    box-shadow: 0 0 5px 2px #cccccc;
+    border: 1px solid #cccccc;
+    border-bottom: 0px;
   }
   .card {
     height: 50px;
@@ -43,7 +44,7 @@
   }
 
   #inbox-iframe {
-    height: calc(100vh - 130px);
+    height: calc(100vh - 57px);
   }
 
   .half_width {
   }
   .card-wrapper {
   }
+
+  #page-wrapper{
+    padding: 0px;
+  }
+
+  .read_notification{
+    background-color: #efefef;
+  }
 </style>
 
 <div class="inbox-panel">
   <div class="section-panel">
+    <h4>New:</h4>
     <div class="card-container">
-      {% for notification in notifications  %}
+      {% for notification in unread_notifications  %}
         <div class="inbox-entry card" onclick="showmessage({{notification.id}}); setactive(this);">
           {{ notification }}
         </div>
       {% endfor %}
     </div>
+    <h4>Read:</h4>
+    <div class="card-container">
+      {% for notification in read_notifications %}
+        <div class="inbox-entry card read_notification" onclick="showmessage({{notification.id}}); setactive(this);">
+          {{ notification }}
+        </div>
+      {% endfor %}
+    </div>
+  </div>
+  <div>
   </div>
   <div class="iframe-panel inbox-expanded-view">
       <div class="inbox-iframe-div">
-        <iframe id="inbox-iframe" frameBorder="0" width="100%" height="100vh" scrolling="yes" onload="sizetoiframe(this);">Please select a notification</iframe>
+        <iframe id="inbox-iframe" frameBorder="0" width="100%" height="100vh" scrolling="yes">Please select a notification</iframe>
       </div>
   </div>
 </div>
 
 <script type="text/javascript">
-  $('#inbox-iframe').load(function() {
-    sizetoiframe(this);
-  })
 
   function showmessage(msg_id)
   {
@@ -82,5 +99,4 @@
   }
 
 </script>
-
 {% endblock %}
index 65d26c9..0eafa60 100644 (file)
@@ -2,19 +2,55 @@
 {% block extrahead %}
 <base target="_parent">
 {% endblock %}
+
 {% block basecontent %}
-<div class="card-container">
-<h3 class="msg_header">{{notification.title}}</h3>
-<p class="content"></p>
-<pre>
-{{notification.content|safe}}
-</pre>
+<script>
+    function send_request(post_data){
+        var form = $("#notification_action_form");
+        var formData = form.serialize() + '&' + post_data + '=true';
+        var req = new XMLHttpRequest();
+        req.open("POST", ".", false);
+        req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+        req.onerror = function() { alert("problem occurred while trying to cancel current workflow"); }
+        req.onreadystatechange = function() { if(req.readyState === 4){
+            window.top.location.href += '';
+        }};
+        req.send(formData);
+    }
+    function delete_notification()
+    {
+        send_request("delete");
+    }
+    function mark_unread()
+    {
+        send_request("unread");
+    }
+</script>
 
+<div>
+    <h3 class="msg_header">{{notification.title}}
+    <div class="btn_group">
+        <button class="btn btn-primary inbox-btn" onclick="mark_unread()">Mark Unread</button>
+        <button class="btn btn-danger inbox-btn" onclick="delete_notification()">Delete</button>
+    </div>
+    </h3>
 </div>
 
+<p class="content-divider"></p>
+
+{% if not notification.is_html %}
+<pre>
+{% endif %}
+    {{notification.content|safe}}
+{% if not notification.is_html %}
+</pre>
+{% endif %}
+<form id="notification_action_form" action="." method="post">
+    {% csrf_token %}
+</form>
+
 <style media="screen">
   .card-container {
-    box-shadow: 0 0 5px 2px #cccccc;
     border: 1px solid #ffffff;
     margin-top: 11px;
   }
     background-color: #ffffff;
     z-index: 5;
   }
-
   .sender {
     color: #636363;
   }
-
-
+  .content-divider {
+    border-bottom: 1px solid #cccccc;
+    padding-bottom: 15px;
+    clear: right;
+  }
+  .inbox-btn{
+    display: inline;
+    margin: 3px;
+  }
+  .btn_group{
+    float: right;
+  }
 </style>
 {% endblock %}
index 9ddb4b8..f78bc01 100644 (file)
             var page_rect = document.getElementById("wrapper").getBoundingClientRect();
             var title_rect = document.getElementById("iframe_header").getBoundingClientRect();
             var iframe_height = page_rect.bottom - title_rect.bottom;
-            console.log("setting height to " + iframe_height);
             document.getElementById("viewport-iframe").height = iframe_height;
 
         }
index 7dae279..cdfddef 100644 (file)
@@ -21,6 +21,7 @@ from api.models import JobFactory
 from dashboard.exceptions import ResourceAvailabilityException, ModelValidationException
 from resource_inventory.models import Image, GenericInterface
 from resource_inventory.resource_manager import ResourceManager
+from resource_inventory.pdf_templater import PDFTemplater
 from notifier.manager import NotificationHandler
 from booking.models import Booking
 
@@ -577,7 +578,7 @@ class Repository():
             booking.collaborators.add(collaborator)
 
         try:
-            booking.pdf = ResourceManager().makePDF(booking.resource)
+            booking.pdf = PDFTemplater.makePDF(booking.resource)
             booking.save()
         except Exception as e:
             return "BOOK, failed to create Pod Desriptor File: " + str(e)
diff --git a/test.sh b/test.sh
index 7931cf0..0fbfd0e 100755 (executable)
--- a/test.sh
+++ b/test.sh
@@ -13,4 +13,4 @@ find . -type f -name "*.py" -not -name "manage.py" | xargs flake8 --count --igno
 
 
 # this file should be executed from the dir it is in
-docker exec -it dg01 python manage.py test -t ../src/
+docker exec -it dg01 python manage.py test