diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/account/urls.py | 19 | ||||
-rw-r--r-- | src/account/views.py | 86 | ||||
-rw-r--r-- | src/booking/migrations/0004_auto_20190124_1700.py | 20 | ||||
-rw-r--r-- | src/booking/models.py | 2 | ||||
-rw-r--r-- | src/resource_inventory/migrations/0006_auto_20190124_1700.py | 76 | ||||
-rw-r--r-- | src/resource_inventory/models.py | 37 | ||||
-rw-r--r-- | src/static/css/detail_view.css | 13 | ||||
-rw-r--r-- | src/templates/account/booking_list.html | 57 | ||||
-rw-r--r-- | src/templates/account/configuration_list.html | 77 | ||||
-rw-r--r-- | src/templates/account/image_list.html | 109 | ||||
-rw-r--r-- | src/templates/account/resource_list.html | 121 | ||||
-rw-r--r-- | src/workflow/models.py | 23 |
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">×</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">×</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">×</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">×</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): |