Merge "Fixed all Flake8 errors"
authorParker Berberian <pberberian@iol.unh.edu>
Mon, 18 Feb 2019 17:07:47 +0000 (17:07 +0000)
committerGerrit Code Review <gerrit@opnfv.org>
Mon, 18 Feb 2019 17:07:47 +0000 (17:07 +0000)
12 files changed:
src/account/urls.py
src/account/views.py
src/booking/migrations/0004_auto_20190124_1700.py [new file with mode: 0644]
src/booking/models.py
src/resource_inventory/migrations/0006_auto_20190124_1700.py [new file with mode: 0644]
src/resource_inventory/models.py
src/static/css/detail_view.css
src/templates/account/booking_list.html
src/templates/account/configuration_list.html
src/templates/account/image_list.html
src/templates/account/resource_list.html
src/workflow/models.py

index 85f0f1a..8aad80c 100644 (file)
@@ -25,6 +25,7 @@ Including another URLconf
     2. Add a URL to urlpatterns:  url(r'^blog/', include('blog.urls'))
 """
 from django.conf.urls import url
+from django.urls import path
 
 from account.views import (
     AccountSettingsView,
@@ -36,7 +37,11 @@ from account.views import (
     account_booking_view,
     account_images_view,
     account_configuration_view,
-    account_detail_view
+    account_detail_view,
+    resource_delete_view,
+    booking_cancel_view,
+    image_delete_view,
+    configuration_delete_view
 )
 
 app_name = "account"
@@ -46,9 +51,13 @@ urlpatterns = [
     url(r'^login/$', JiraLoginView.as_view(), name='login'),
     url(r'^logout/$', JiraLogoutView.as_view(), name='logout'),
     url(r'^users/$', UserListView.as_view(), name='users'),
-    url(r'^my/resources', account_resource_view, name="my-resources"),
-    url(r'^my/bookings', account_booking_view, name="my-bookings"),
-    url(r'^my/images', account_images_view, name="my-images"),
-    url(r'^my/configurations', account_configuration_view, name="my-configurations"),
+    url(r'^my/resources/$', account_resource_view, name="my-resources"),
+    path('my/resources/delete/<int:resource_id>', resource_delete_view),
+    url(r'^my/bookings/$', account_booking_view, name="my-bookings"),
+    path('my/bookings/cancel/<int:booking_id>', booking_cancel_view),
+    url(r'^my/images/$', account_images_view, name="my-images"),
+    path('my/images/delete/<int:image_id>', image_delete_view),
+    url(r'^my/configurations/$', account_configuration_view, name="my-configurations"),
+    path('my/configurations/delete/<int:config_id>', configuration_delete_view),
     url(r'^my/$', account_detail_view, name="my-account"),
 ]
index e880208..11689a1 100644 (file)
@@ -20,6 +20,9 @@ from django.contrib.auth.decorators import login_required
 from django.contrib.auth.mixins import LoginRequiredMixin
 from django.contrib.auth.models import User
 from django.urls import reverse
+from django.http import HttpResponse
+from django.utils import timezone
+from django.shortcuts import get_object_or_404
 from django.utils.decorators import method_decorator
 from django.views.generic import RedirectView, TemplateView, UpdateView
 from django.shortcuts import render
@@ -30,7 +33,7 @@ from account.forms import AccountSettingsForm
 from account.jira_util import SignatureMethod_RSA_SHA1
 from account.models import UserProfile
 from booking.models import Booking
-from resource_inventory.models import GenericResourceBundle, ConfigBundle, Image
+from resource_inventory.models import GenericResourceBundle, ConfigBundle, Image, Host
 
 
 @method_decorator(login_required, name='dispatch')
@@ -172,8 +175,22 @@ def account_resource_view(request):
     if not request.user.is_authenticated:
         return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
     template = "account/resource_list.html"
-    resources = list(GenericResourceBundle.objects.filter(owner=request.user))
-    context = {"resources": resources, "title": "My Resources"}
+    resources = GenericResourceBundle.objects.filter(
+        owner=request.user).prefetch_related("configbundle_set")
+    mapping = {}
+    resource_list = []
+    booking_mapping = {}
+    for grb in resources:
+        resource_list.append(grb)
+        mapping[grb.id] = [{"id": x.id, "name": x.name} for x in grb.configbundle_set.all()]
+        if Booking.objects.filter(resource__template=grb, end__gt=timezone.now()).exists():
+            booking_mapping[grb.id] = "true"
+    context = {
+        "resources": resource_list,
+        "grb_mapping": mapping,
+        "booking_mapping": booking_mapping,
+        "title": "My Resources"
+    }
     return render(request, template, context=context)
 
 
@@ -202,5 +219,66 @@ def account_images_view(request):
     template = "account/image_list.html"
     my_images = Image.objects.filter(owner=request.user)
     public_images = Image.objects.filter(public=True)
-    context = {"title": "Images", "images": my_images, "public_images": public_images}
+    used_images = {}
+    for image in my_images:
+        if Host.objects.filter(booked=True, config__image=image).exists():
+            used_images[image.id] = "true"
+    context = {
+        "title": "Images",
+        "images": my_images,
+        "public_images": public_images,
+        "used_images": used_images
+        }
     return render(request, template, context=context)
+
+
+def resource_delete_view(request, resource_id=None):
+    if not request.user.is_authenticated:
+        return HttpResponse('no')  # 403?
+    grb = get_object_or_404(GenericResourceBundle, pk=resource_id)
+    if not request.user.id == grb.owner.id:
+        return HttpResponse('no')  # 403?
+    if Booking.objects.filter(resource__template=grb, end__gt=timezone.now()).exists():
+        return HttpResponse('no')  # 403?
+    grb.delete()
+    return HttpResponse('')
+
+
+def configuration_delete_view(request, config_id=None):
+    if not request.user.is_authenticated:
+        return HttpResponse('no')  # 403?
+    config = get_object_or_404(ConfigBundle, pk=config_id)
+    if not request.user.id == config.owner.id:
+        return HttpResponse('no')  # 403?
+    if Booking.objects.filter(config_bundle=config, end__gt=timezone.now()).exists():
+        return HttpResponse('no')
+    config.delete()
+    return HttpResponse('')
+
+
+def booking_cancel_view(request, booking_id=None):
+    if not request.user.is_authenticated:
+        return HttpResponse('no')  # 403?
+    booking = get_object_or_404(Booking, pk=booking_id)
+    if not request.user.id == booking.owner.id:
+        return HttpResponse('no')  # 403?
+
+    if booking.end < timezone.now():  # booking already over
+        return HttpResponse('')
+
+    booking.end = timezone.now()
+    booking.save()
+    return HttpResponse('')
+
+
+def image_delete_view(request, image_id=None):
+    if not request.user.is_authenticated:
+        return HttpResponse('no')  # 403?
+    image = get_object_or_404(Image, pk=image_id)
+    if image.public or image.owner.id != request.user.id:
+        return HttpResponse('no')  # 403?
+    # check if used in booking
+    if Host.objects.filter(booked=True, config__image=image).exists():
+        return HttpResponse('no')  # 403?
+    image.delete()
+    return HttpResponse('')
diff --git a/src/booking/migrations/0004_auto_20190124_1700.py b/src/booking/migrations/0004_auto_20190124_1700.py
new file mode 100644 (file)
index 0000000..baa32d2
--- /dev/null
@@ -0,0 +1,20 @@
+# Generated by Django 2.1 on 2019-01-24 17:00
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('booking', '0003_auto_20190115_1733'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='booking',
+            name='owner',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='owner', to=settings.AUTH_USER_MODEL),
+        ),
+    ]
index 0972922..8612abd 100644 (file)
@@ -18,7 +18,7 @@ import resource_inventory.resource_manager
 
 class Booking(models.Model):
     id = models.AutoField(primary_key=True)
-    owner = models.ForeignKey(User, models.CASCADE, related_name='owner')
+    owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='owner')
     collaborators = models.ManyToManyField(User, related_name='collaborators')
     start = models.DateTimeField()
     end = models.DateTimeField()
diff --git a/src/resource_inventory/migrations/0006_auto_20190124_1700.py b/src/resource_inventory/migrations/0006_auto_20190124_1700.py
new file mode 100644 (file)
index 0000000..a5a972f
--- /dev/null
@@ -0,0 +1,76 @@
+# Generated by Django 2.1 on 2019-01-24 17:00
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import resource_inventory.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('resource_inventory', '0005_image_os'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='cpuprofile',
+            name='host',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cpuprofile', to='resource_inventory.HostProfile'),
+        ),
+        migrations.AlterField(
+            model_name='diskprofile',
+            name='host',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='storageprofile', to='resource_inventory.HostProfile'),
+        ),
+        migrations.AlterField(
+            model_name='generichost',
+            name='profile',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.HostProfile'),
+        ),
+        migrations.AlterField(
+            model_name='generichost',
+            name='resource',
+            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='generic_host', to='resource_inventory.GenericResource'),
+        ),
+        migrations.AlterField(
+            model_name='genericinterface',
+            name='host',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='generic_interfaces', to='resource_inventory.GenericHost'),
+        ),
+        migrations.AlterField(
+            model_name='genericresource',
+            name='bundle',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='generic_resources', to='resource_inventory.GenericResourceBundle'),
+        ),
+        migrations.AlterField(
+            model_name='genericresourcebundle',
+            name='lab',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='account.Lab'),
+        ),
+        migrations.AlterField(
+            model_name='genericresourcebundle',
+            name='owner',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AlterField(
+            model_name='hostconfiguration',
+            name='opnfvRole',
+            field=models.ForeignKey(on_delete=models.SET(resource_inventory.models.get_sentinal_opnfv_role), to='resource_inventory.OPNFVRole'),
+        ),
+        migrations.AlterField(
+            model_name='interfaceprofile',
+            name='host',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interfaceprofile', to='resource_inventory.HostProfile'),
+        ),
+        migrations.AlterField(
+            model_name='ramprofile',
+            name='host',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ramprofile', to='resource_inventory.HostProfile'),
+        ),
+        migrations.AlterField(
+            model_name='resourcebundle',
+            name='template',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.GenericResourceBundle'),
+        ),
+    ]
index 5b07077..e1f2aa3 100644 (file)
@@ -39,7 +39,7 @@ class InterfaceProfile(models.Model):
     id = models.AutoField(primary_key=True)
     speed = models.IntegerField()
     name = models.CharField(max_length=100)
-    host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='interfaceprofile')
+    host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='interfaceprofile')
     nic_type = models.CharField(
         max_length=50,
         choices=[
@@ -61,7 +61,7 @@ class DiskProfile(models.Model):
         ("HDD", "HDD")
     ])
     name = models.CharField(max_length=50)
-    host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='storageprofile')
+    host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='storageprofile')
     rotation = models.IntegerField(default=0)
     interface = models.CharField(
         max_length=50,
@@ -88,7 +88,7 @@ class CpuProfile(models.Model):
         ("aarch64", "aarch64")
     ])
     cpus = models.IntegerField()
-    host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='cpuprofile')
+    host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='cpuprofile')
     cflags = models.TextField(null=True)
 
     def __str__(self):
@@ -99,7 +99,7 @@ class RamProfile(models.Model):
     id = models.AutoField(primary_key=True)
     amount = models.IntegerField()
     channels = models.IntegerField()
-    host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='ramprofile')
+    host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='ramprofile')
 
     def __str__(self):
         return str(self.amount) + "G for " + str(self.host)
@@ -130,8 +130,8 @@ class GenericResourceBundle(models.Model):
     id = models.AutoField(primary_key=True)
     name = models.CharField(max_length=300, unique=True)
     xml = models.TextField()
-    owner = models.ForeignKey(User, null=True, on_delete=models.DO_NOTHING)
-    lab = models.ForeignKey(Lab, null=True, on_delete=models.DO_NOTHING)
+    owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
+    lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL)
     description = models.CharField(max_length=1000, default="")
 
     def getHosts(self):
@@ -146,7 +146,7 @@ class GenericResourceBundle(models.Model):
 
 
 class GenericResource(models.Model):
-    bundle = models.ForeignKey(GenericResourceBundle, related_name='generic_resources', on_delete=models.DO_NOTHING)
+    bundle = models.ForeignKey(GenericResourceBundle, related_name='generic_resources', on_delete=models.CASCADE)
     hostname_validchars = RegexValidator(regex=r'(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))', message="Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)")
     name = models.CharField(max_length=200, validators=[hostname_validchars])
 
@@ -167,8 +167,8 @@ class GenericResource(models.Model):
 # Host template
 class GenericHost(models.Model):
     id = models.AutoField(primary_key=True)
-    profile = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING)
-    resource = models.OneToOneField(GenericResource, related_name='generic_host', on_delete=models.DO_NOTHING)
+    profile = models.ForeignKey(HostProfile, on_delete=models.CASCADE)
+    resource = models.OneToOneField(GenericResource, related_name='generic_host', on_delete=models.CASCADE)
 
     def __str__(self):
         return self.resource.name
@@ -177,9 +177,11 @@ class GenericHost(models.Model):
 # Physical, actual resources
 class ResourceBundle(models.Model):
     id = models.AutoField(primary_key=True)
-    template = models.ForeignKey(GenericResourceBundle, on_delete=models.DO_NOTHING)
+    template = models.ForeignKey(GenericResourceBundle, on_delete=models.SET_NULL, null=True)
 
     def __str__(self):
+        if self.template is None:
+            return "Resource bundle " + str(self.id) + " with no template"
         return "instance of " + str(self.template)
 
 
@@ -189,8 +191,8 @@ class ResourceBundle(models.Model):
 class GenericInterface(models.Model):
     id = models.AutoField(primary_key=True)
     vlans = models.ManyToManyField(Vlan)
-    profile = models.ForeignKey(InterfaceProfile, on_delete=models.DO_NOTHING)
-    host = models.ForeignKey(GenericHost, on_delete=models.DO_NOTHING, related_name='generic_interfaces')
+    profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE)
+    host = models.ForeignKey(GenericHost, on_delete=models.CASCADE, related_name='generic_interfaces')
 
     def __str__(self):
         return "type " + str(self.profile) + " on host " + str(self.host)
@@ -224,7 +226,7 @@ class Opsys(models.Model):
 
 class ConfigBundle(models.Model):
     id = models.AutoField(primary_key=True)
-    owner = models.ForeignKey(User, on_delete=models.CASCADE)  # consider setting to root user?
+    owner = models.ForeignKey(User, on_delete=models.CASCADE)
     name = models.CharField(max_length=200, unique=True)
     description = models.CharField(max_length=1000, default="")
     bundle = models.ForeignKey(GenericResourceBundle, null=True, on_delete=models.CASCADE)
@@ -262,15 +264,18 @@ class Image(models.Model):
     name = models.CharField(max_length=200)
     owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
     public = models.BooleanField(default=True)
-    # may need to change host_type.on_delete to models.SET() once images are transferrable between compatible host types
     host_type = models.ForeignKey(HostProfile, on_delete=models.CASCADE)
     description = models.TextField()
-    os = models.ForeignKey(Opsys, null=True, on_delete=models.CASCADE)
+    os = models.ForeignKey(Opsys, null=True, on_delete=models.CASCADE) #sentinal?
 
     def __str__(self):
         return self.name
 
 
+def get_sentinal_opnfv_role():
+    return OPNFVRole.objects.get_or_create(name="deleted", description="Role was deleted.")
+
+
 class HostConfiguration(models.Model):
     """
     model to represent a complete configuration for a single
@@ -280,7 +285,7 @@ class HostConfiguration(models.Model):
     host = models.ForeignKey(GenericHost, related_name="configuration", on_delete=models.CASCADE)
     image = models.ForeignKey(Image, on_delete=models.PROTECT)
     bundle = models.ForeignKey(ConfigBundle, related_name="hostConfigurations", null=True, on_delete=models.CASCADE)
-    opnfvRole = models.ForeignKey(OPNFVRole, on_delete=models.PROTECT)
+    opnfvRole = models.ForeignKey(OPNFVRole, on_delete=models.SET(get_sentinal_opnfv_role))
 
     def __str__(self):
         return "config with " + str(self.host) + " and image " + str(self.image)
index 69a2643..7948d85 100644 (file)
     font-size: 16px;
 }
 
+.detail_button_container .btn {
+    width: 49%;
+}
+
+.detail_button_container .btn-danger {
+    float: right;
+}
+
+#modal_warning {
+    transition: max-height 0.5s ease-out;
+    overflow: hidden;
+}
+
 .detail_card {
     border: 2px;
     border-color: black;
index 9c6f3db..e56b19e 100644 (file)
                     <li class="list-group-item">purpose: {{booking.purpose}}</li>
                 </ul>
             </div>
-            <a class="btn btn-primary" href="/booking/detail/{{booking.id}}/">Details</a>
+            <div class="detail_button_container">
+                <a class="btn btn-primary" href="/booking/detail/{{booking.id}}/">Details</a>
+                <button
+                    class="btn btn-danger"
+                    onclick='cancel_booking({{booking.id}});'
+                    data-toggle="modal"
+                    data-target="#resModal"
+                >Cancel</button>
+            </div>
         </div>
     {% endfor %}
     </div>
         </div>
     {% endfor %}
     </div>
+
+<script>
+    var current_booking_id = -1;
+    function cancel_booking(booking_id) {
+        current_booking_id = booking_id;
+        document.getElementById('modal_warning').style['max-height'] = '0px';
+    }
+
+    function submit_cancel_form() {
+        var ajaxForm = $("#booking_cancel_form");
+        var formData = ajaxForm.serialize();
+        req = new XMLHttpRequest();
+        var url = "cancel/" + current_booking_id;
+        req.open("POST", url, true);
+        req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+        req.onerror = function() { alert("problem submitting form"); }
+        req.send(formData);
+    }
+</script>
+<div class="modal fade" id="resModal" tabindex="-1" role="dialog" aria-labelledby="my_modal" aria-hidden="true">
+    <div class="modal-dialog" style="width: 450px;" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h4 class="modal-title" id="my_modal" style="display: inline; float: left;">Cancel Booking?</h4>
+                <p>Everthing on your machine(s) will be lost</p>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <form id="booking_cancel_form">
+                {% csrf_token %}
+            </form>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+                <button type="button" class="btn btn-primary" onclick="document.getElementById('modal_warning').style['max-height'] = '500px';">Cancel Booking</button>
+            </div>
+            <div id="modal_warning" class="modal-footer" style="max-height:0px;" >
+                <div style="text-align:center; margin: 5px">
+                    <h3>Are You Sure?</h3>
+                    <p>This cannot be undone</p>
+                    <button class="btn" onclick="document.getElementById('modal_warning').style['max-height'] = '0px';">Nevermind</button>
+                    <button class="btn btn-danger" id="confirm_cancel_button" data-dismiss="modal" onclick="submit_cancel_form();">I'm Sure</button>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
 {% endblock %}
index 14d0472..b920ba6 100644 (file)
@@ -1,18 +1,71 @@
 {% extends "base.html" %}
 {% block content %}
-    <div class="card_container">
-    {% for config in configurations %}
-        <div class="detail_card">
-            <div>
-                <h3>Configuration {{config.id}}</h3>
-                <ul class="list-group">
-                    <li class="list-group-item">id: {{config.id}}</li>
-                    <li class="list-group-item">name: {{config.name}}</li>
-                    <li class="list-group-item">description: {{config.description}}</li>
-                    <li class="list-group-item">resource: {{config.bundle}}</li>
-                </ul>
+<div class="card_container">
+{% for config in configurations %}
+    <div class="detail_card">
+        <div>
+            <h3>Configuration {{config.id}}</h3>
+            <ul class="list-group">
+                <li class="list-group-item">id: {{config.id}}</li>
+                <li class="list-group-item">name: {{config.name}}</li>
+                <li class="list-group-item">description: {{config.description}}</li>
+                <li class="list-group-item">resource: {{config.bundle}}</li>
+            </ul>
+        </div>
+        <div class="detail_button_container">
+            <button
+                class="btn btn-danger"
+                style="width:49%;float:right;"
+                onclick='delete_config({{config.id}});'
+                data-toggle="modal"
+                data-target="#configModal"
+            >Delete</button>
+        </div>
+    </div>
+{% endfor %}
+</div>
+<script>
+    var current_config_id = -1;
+    function delete_config(config_id) {
+        current_config_id = config_id;
+        document.getElementById('modal_warning').style['max-height'] = '0px';
+    }
+
+    function submit_delete_form() {
+        var ajaxForm = $("#config_delete_form");
+        var formData = ajaxForm.serialize();
+        req = new XMLHttpRequest();
+        var url = "delete/" + current_config_id;
+        req.open("POST", url, true);
+        req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+        req.onerror = function() { alert("problem submitting form"); }
+        req.send(formData);
+    }
+</script>
+<div class="modal fade" id="configModal" tabindex="-1" role="dialog" aria-labelledby="my_modal" aria-hidden="true">
+    <div class="modal-dialog" style="width: 450px;" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h4 class="modal-title" id="my_modal" style="display: inline; float: left;">Delete Configuration?</h4>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <form id="config_delete_form">
+                {% csrf_token %}
+            </form>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+                <button type="button" class="btn btn-primary" onclick="document.getElementById('modal_warning').style['max-height'] = '500px';">Delete</button>
             </div>
+            <div id="modal_warning" class="modal-footer" style="max-height:0px;" >
+                <div style="text-align:center; margin: 5px">
+                    <h3>Are You Sure?</h3>
+                    <p>This cannot be undone</p>
+                    <button class="btn" onclick="document.getElementById('modal_warning').style['max-height'] = '0px';">Nevermind</button>
+                    <button class="btn btn-danger" data-dismiss="modal" onclick="submit_delete_form();">I'm Sure</button>
+                </div>
         </div>
-    {% endfor %}
     </div>
+</div>
 {% endblock %}
index 7566a9c..cd83dcf 100644 (file)
@@ -2,20 +2,29 @@
 {% block content %}
 <h2>Images I Own</h2>
 <div class="card_container">
-    {% for image in images %}
-        <div class="detail_card">
-            <div>
-                <h3>Image {{image.id}}</h3>
-                <ul class="list-group">
-                    <li class="list-group-item">id: {{image.id}}</li>
-                    <li class="list-group-item">lab: {{image.from_lab.name}}</li>
-                    <li class="list-group-item">name: {{image.name}}</li>
-                    <li class="list-group-item">description: {{image.description}}</li>
-                    <li class="list-group-item">host profile: {{image.host_type.name}}</li>
-                </ul>
-            </div>
+{% for image in images %}
+    <div class="detail_card">
+        <div>
+            <h3>Image {{image.id}}</h3>
+            <ul class="list-group">
+                <li class="list-group-item">id: {{image.id}}</li>
+                <li class="list-group-item">lab: {{image.from_lab.name}}</li>
+                <li class="list-group-item">name: {{image.name}}</li>
+                <li class="list-group-item">description: {{image.description}}</li>
+                <li class="list-group-item">host profile: {{image.host_type.name}}</li>
+            </ul>
         </div>
-    {% endfor %}
+        <div class="detail_button_container">
+            <button
+                class="btn btn-danger"
+                style="width:49%;float:right;"
+                onclick='delete_image({{image.id}});'
+                data-toggle="modal"
+                data-target="#imageModal"
+            >Delete</button>
+        </div>
+    </div>
+{% endfor %}
 </div>
 <h2>Public Images</h2>
 <div class="card_container">
         </div>
     {% endfor %}
 </div>
+
+<script>
+    var current_image_id = -1;
+    var used_images = {{used_images|safe|default:"{}"}};
+    function delete_image(image_id) {
+        current_image_id = image_id;
+        document.getElementById('modal_warning').style['max-height'] = '0px';
+        var warning_header = document.getElementById("warning_header");
+        var warning_text = document.getElementById("warning_text");
+        var delete_image_button = document.getElementById("final_delete_b");
+        clear(warning_header);
+        clear(warning_text);
+        if(used_images[image_id]) {
+            warning_header.appendChild(
+                document.createTextNode("Cannot Delete")
+            );
+            warning_text.appendChild(
+                document.createTextNode("This snapshot is being used in a booking.")
+            );
+            delete_image_button.disabled = true;
+        } else {
+            warning_header.appendChild(
+                document.createTextNode("Are You Sure?")
+            );
+            warning_text.appendChild(
+                document.createTextNode("This cannot be undone")
+            );
+            delete_image_button.removeAttribute("disabled");
+        }
+    }
+
+    function submit_delete_form() {
+        var ajaxForm = $("#image_delete_form");
+        var formData = ajaxForm.serialize();
+        req = new XMLHttpRequest();
+        var url = "delete/" + current_image_id;
+        req.open("POST", url, true);
+        req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+        req.onerror = function() { alert("problem submitting form"); }
+        req.send(formData);
+    }
+
+    function clear(node) {
+        while(node.lastChild) {
+            node.removeChild(node.lastChild);
+        }
+    }
+</script>
+<div class="modal fade" id="imageModal" tabindex="-1" role="dialog" aria-labelledby="my_modal" aria-hidden="true">
+    <div class="modal-dialog" style="width: 450px;" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h4 class="modal-title" id="my_modal" style="display: inline; float: left;">Delete Configuration?</h4>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <form id="image_delete_form">
+                {% csrf_token %}
+            </form>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+                <button type="button" class="btn btn-primary" onclick="document.getElementById('modal_warning').style['max-height'] = '500px';">Delete</button>
+            </div>
+            <div id="modal_warning" class="modal-footer" style="max-height:0px;" >
+                <div style="text-align:center; margin: 5px">
+                    <h3 id="warning_header">Are You Sure?</h3>
+                    <p id="warning_text">This cannot be undone</p>
+                    <button class="btn" onclick="document.getElementById('modal_warning').style['max-height'] = '0px';">Nevermind</button>
+                    <button id="final_delete_b" class="btn btn-danger" data-dismiss="modal" onclick="submit_delete_form();">I'm Sure</button>
+                </div>
+        </div>
+    </div>
+</div>
 {% endblock %}
index cdacdd6..1391e8e 100644 (file)
 {% extends "base.html" %}
 {% block content %}
-    <div class="card_container">
-    {% for resource in resources %}
-        <div class="detail_card">
-            <div>
-                <h3>Resource {{resource.id}}</h3>
-                <ul class="list-group">
-                    <li class="list-group-item">id: {{resource.id}}</li>
-                    <li class="list-group-item">name: {{resource.name}}</li>
-                    <li class="list-group-item">description: {{resource.description}}</li>
-                </ul>
+<div class="card_container">
+{% for resource in resources %}
+    <div class="detail_card">
+        <div>
+            <h3>Resource {{resource.id}}</h3>
+            <ul class="list-group">
+                <li class="list-group-item">id: {{resource.id}}</li>
+                <li class="list-group-item">name: {{resource.name}}</li>
+                <li class="list-group-item">description: {{resource.description}}</li>
+            </ul>
+        </div>
+        <div class="detail_button_container">
+            <button
+                class="btn btn-danger"
+                onclick='delete_resource({{resource.id}});'
+                data-toggle="modal"
+                data-target="#resModal"
+            >Delete</button>
+        </div>
+    </div>
+{% endfor %}
+</div>
+<script>
+    var grb_mapping = {{grb_mapping|safe|default:"{}"}};
+    var booking_mapping = {{booking_mapping|safe|default:"{}"}};
+    var current_resource_id = -1;
+    function delete_resource(resource_id) {
+        document.getElementById("confirm_delete_button").removeAttribute("disabled");
+        var configs = grb_mapping[resource_id];
+        var warning = document.createTextNode("Are You Sure?");
+        var warning_subtext = document.createTextNode("This cannot be undone");
+        if(booking_mapping[resource_id]){
+            var warning = document.createTextNode("This resource is being used. It cannot be deleted.");
+            var warning_subtext = document.createTextNode("If your booking just ended, you may need to give us a few minutes to clean it up before this can be removed.");
+
+            document.getElementById("confirm_delete_button").disabled = true;
+        }
+        else if(configs.length > 0) {
+            list_configs(configs);
+            warning_text = "Are You Sure? The following Configurations will also be deleted.";
+            warning = document.createTextNode(warning_text);
+        }
+
+        current_resource_id = resource_id;
+        set_modal_text(warning, warning_subtext);
+    }
+
+    function set_modal_text(title, text) {
+        var clear = function(node) {
+            while(node.lastChild) {
+            node.removeChild(node.lastChild);
+            }
+        }
+        var warning_title = document.getElementById("config_warning");
+        var warning_text = document.getElementById("warning_subtext");
+
+        clear(warning_title);
+        clear(warning_text);
+
+        warning_title.appendChild(title);
+        warning_text.appendChild(text);
+        document.getElementById('modal_warning').style['max-height'] = '0px';
+    }
+
+    function list_configs(configs) {
+        var list = document.getElementById("config_list");
+        for(var i=0; i<configs.length; i++){
+            var str = configs[i].name;
+            var list_item = document.createElement("LI");
+            list_item.appendChild(document.createTextNode(str));
+            list.appendChild(list_item);
+        }
+    }
+
+    function submit_delete_form() {
+        var ajaxForm = $("#res_delete_form");
+        var formData = ajaxForm.serialize();
+        req = new XMLHttpRequest();
+        var url = "delete/" + current_resource_id;
+        req.open("POST", url, true);
+        req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+        req.onerror = function() { alert("problem submitting form"); }
+        req.send(formData);
+    }
+</script>
+<div class="modal fade" id="resModal" tabindex="-1" role="dialog" aria-labelledby="my_modal" aria-hidden="true">
+    <div class="modal-dialog" style="width: 450px;" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h4 class="modal-title" id="my_modal" style="display: inline; float: left;">Delete Resource?</h4>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <form id="res_delete_form">
+                {% csrf_token %}
+            </form>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+                <button type="button" class="btn btn-primary" onclick="document.getElementById('modal_warning').style['max-height'] = '500px';">Delete</button>
             </div>
+            <div id="modal_warning" class="modal-footer" style="max-height:0px;" >
+                <div style="text-align:center; margin: 5px">
+                    <h3 id="config_warning">Are You Sure?</h3>
+                    <p id="warning_subtext">This cannot be undone</p>
+                    <ul id="config_list"></ul>
+                    <button class="btn" onclick="document.getElementById('modal_warning').style['max-height'] = '0px';">Nevermind</button>
+                    <button class="btn btn-danger" id="confirm_delete_button" data-dismiss="modal" onclick="submit_delete_form();">I'm Sure</button>
+                </div>
         </div>
-    {% endfor %}
     </div>
+</div>
+
 {% endblock %}
index 21f3a8e..7dae279 100644 (file)
@@ -61,15 +61,25 @@ class BookingAuthManager():
     def parse_gerrit_url(self, url):
         project_leads = []
         try:
-            parts = url.split("/")
+            halfs = url.split("?")
+            parts = halfs[0].split("/")
+            args = halfs[1].split(";")
             if "http" in parts[0]:  # the url include http(s)://
                 parts = parts[2:]
-            if "f=INFO.yaml" not in parts[-1].split(";"):
+            if "f=INFO.yaml" not in args:
                 return None
             if "gerrit.opnfv.org" not in parts[0]:
                 return None
+            try:
+                i = args.index("a=blob")
+                args[i] = "a=blob_plain"
+            except ValueError:
+                pass
+            # recreate url
+            halfs[1] = ";".join(args)
+            halfs[0] = "/".join(parts)
             # now to download and parse file
-            url = "https://" + "/".join(parts)
+            url = "https://" + "?".join(halfs)
             info_file = requests.get(url, timeout=15).text
             info_parsed = yaml.load(info_file)
             ptl = info_parsed.get('project_lead')
@@ -138,8 +148,11 @@ class BookingAuthManager():
             return True  # admin override for this user
         if repo.BOOKING_INFO_FILE not in repo.el:
             return False  # INFO file not provided
-        ptl_info = self.parse_url(repo.BOOKING_INFO_FILE)
-        return ptl_info and ptl_info == booking.owner.userprofile.email_addr
+        ptl_info = self.parse_url(repo.el.get(repo.BOOKING_INFO_FILE))
+        for ptl in ptl_info:
+            if ptl['email'] == booking.owner.userprofile.email_addr:
+                return True
+        return False
 
 
 class WorkflowStep(object):