summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/account/urls.py19
-rw-r--r--src/account/views.py86
-rw-r--r--src/booking/migrations/0004_auto_20190124_1700.py20
-rw-r--r--src/booking/models.py2
-rw-r--r--src/resource_inventory/migrations/0006_auto_20190124_1700.py76
-rw-r--r--src/resource_inventory/models.py37
-rw-r--r--src/static/css/detail_view.css13
-rw-r--r--src/templates/account/booking_list.html57
-rw-r--r--src/templates/account/configuration_list.html77
-rw-r--r--src/templates/account/image_list.html109
-rw-r--r--src/templates/account/resource_list.html121
-rw-r--r--src/workflow/models.py23
12 files changed, 572 insertions, 68 deletions
diff --git a/src/account/urls.py b/src/account/urls.py
index 85f0f1a..8aad80c 100644
--- a/src/account/urls.py
+++ b/src/account/urls.py
@@ -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"),
]
diff --git a/src/account/views.py b/src/account/views.py
index e880208..11689a1 100644
--- a/src/account/views.py
+++ b/src/account/views.py
@@ -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
index 0000000..baa32d2
--- /dev/null
+++ b/src/booking/migrations/0004_auto_20190124_1700.py
@@ -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),
+ ),
+ ]
diff --git a/src/booking/models.py b/src/booking/models.py
index 0972922..8612abd 100644
--- a/src/booking/models.py
+++ b/src/booking/models.py
@@ -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
index 0000000..a5a972f
--- /dev/null
+++ b/src/resource_inventory/migrations/0006_auto_20190124_1700.py
@@ -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'),
+ ),
+ ]
diff --git a/src/resource_inventory/models.py b/src/resource_inventory/models.py
index 5b07077..e1f2aa3 100644
--- a/src/resource_inventory/models.py
+++ b/src/resource_inventory/models.py
@@ -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)
diff --git a/src/static/css/detail_view.css b/src/static/css/detail_view.css
index 69a2643..7948d85 100644
--- a/src/static/css/detail_view.css
+++ b/src/static/css/detail_view.css
@@ -10,6 +10,19 @@
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;
diff --git a/src/templates/account/booking_list.html b/src/templates/account/booking_list.html
index 9c6f3db..e56b19e 100644
--- a/src/templates/account/booking_list.html
+++ b/src/templates/account/booking_list.html
@@ -15,7 +15,15 @@
<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>
@@ -38,4 +46,51 @@
</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 %}
diff --git a/src/templates/account/configuration_list.html b/src/templates/account/configuration_list.html
index 14d0472..b920ba6 100644
--- a/src/templates/account/configuration_list.html
+++ b/src/templates/account/configuration_list.html
@@ -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 %}
diff --git a/src/templates/account/image_list.html b/src/templates/account/image_list.html
index 7566a9c..cd83dcf 100644
--- a/src/templates/account/image_list.html
+++ b/src/templates/account/image_list.html
@@ -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">
@@ -34,4 +43,78 @@
</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 %}
diff --git a/src/templates/account/resource_list.html b/src/templates/account/resource_list.html
index cdacdd6..1391e8e 100644
--- a/src/templates/account/resource_list.html
+++ b/src/templates/account/resource_list.html
@@ -1,17 +1,116 @@
{% 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 %}
diff --git a/src/workflow/models.py b/src/workflow/models.py
index 21f3a8e..7dae279 100644
--- a/src/workflow/models.py
+++ b/src/workflow/models.py
@@ -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):