From 1f3a770d2547848590f39e9d9b9bdffeb94eec14 Mon Sep 17 00:00:00 2001 From: Parker Berberian Date: Wed, 10 Oct 2018 16:06:47 -0400 Subject: Lab as a Service 2.0 See changes here: https://wiki.opnfv.org/display/INF/Pharos+Laas Change-Id: I59ada5f98e70a28d7f8c14eab3239597e236ca26 Signed-off-by: Sawyer Bergeron Signed-off-by: Parker Berberian --- src/booking/__init__.py | 2 - src/booking/admin.py | 4 +- src/booking/forms.py | 65 ------- src/booking/migrations/0001_initial.py | 48 ++--- src/booking/migrations/0002_booking_changeid.py | 38 ---- src/booking/migrations/0003_auto_20180108_2024.py | 25 --- src/booking/migrations/0004_booking_ext_count.py | 27 --- src/booking/migrations/__init__.py | 10 - src/booking/models.py | 60 +++--- src/booking/stats.py | 56 ++++++ src/booking/tests/__init__.py | 2 - src/booking/tests/test_models.py | 216 +++++++++++++++++----- src/booking/tests/test_views.py | 106 ----------- src/booking/urls.py | 16 +- src/booking/views.py | 138 ++++++++------ 15 files changed, 376 insertions(+), 437 deletions(-) delete mode 100644 src/booking/forms.py delete mode 100644 src/booking/migrations/0002_booking_changeid.py delete mode 100644 src/booking/migrations/0003_auto_20180108_2024.py delete mode 100644 src/booking/migrations/0004_booking_ext_count.py create mode 100644 src/booking/stats.py delete mode 100644 src/booking/tests/test_views.py (limited to 'src/booking') diff --git a/src/booking/__init__.py b/src/booking/__init__.py index b5914ce..b6fef6c 100644 --- a/src/booking/__init__.py +++ b/src/booking/__init__.py @@ -6,5 +6,3 @@ # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - - diff --git a/src/booking/admin.py b/src/booking/admin.py index 51e1031..2beb05b 100644 --- a/src/booking/admin.py +++ b/src/booking/admin.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -13,6 +14,3 @@ from django.contrib import admin from booking.models import * admin.site.register(Booking) -admin.site.register(Opsys) -admin.site.register(Installer) -admin.site.register(Scenario) diff --git a/src/booking/forms.py b/src/booking/forms.py deleted file mode 100644 index 9d71b42..0000000 --- a/src/booking/forms.py +++ /dev/null @@ -1,65 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - -import django.forms as forms - -from booking.models import Installer, Scenario, Opsys -from datetime import datetime - -class BookingForm(forms.Form): - fields = ['start', 'end', 'purpose', 'opsys', 'reset', 'installer', 'scenario'] - - start = forms.DateTimeField() - end = forms.DateTimeField() - reset = forms.ChoiceField(choices = ((True, 'Yes'),(False, 'No')), label="Reset System", initial='False', required=False) - purpose = forms.CharField(max_length=300) - opsys = forms.ModelChoiceField(queryset=Opsys.objects.all(), required=False) - opsys.label = "Operating System" - installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=False) - scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=False) - -class BookingEditForm(forms.Form): - fields = ['start', 'end', 'purpose', 'opsys', 'reset', 'installer', 'scenario'] - - start = forms.DateTimeField() - end = forms.DateTimeField() - purpose = forms.CharField(max_length=300) - opsys = forms.ModelChoiceField(queryset=Opsys.objects.all(), required=False) - installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=False) - scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=False) - reset = forms.ChoiceField(choices = ((True, 'Yes'),(False, 'No')), label="Reset System", initial='False', required=True) - - - def __init__(self, *args, **kwargs ): - cloned_kwargs = {} - cloned_kwargs['purpose'] = kwargs.pop('purpose') - cloned_kwargs['start'] = kwargs.pop('start') - cloned_kwargs['end'] = kwargs.pop('end') - if 'installer' in kwargs: - cloned_kwargs['installer'] = kwargs.pop('installer') - if 'scenario' in kwargs: - cloned_kwargs['scenario'] = kwargs.pop('scenario') - super(BookingEditForm, self).__init__( *args, **kwargs) - - self.fields['purpose'].initial = cloned_kwargs['purpose'] - self.fields['start'].initial = cloned_kwargs['start'].strftime('%m/%d/%Y %H:%M') - self.fields['end'].initial = cloned_kwargs['end'].strftime('%m/%d/%Y %H:%M') - try: - self.fields['installer'].initial = cloned_kwargs['installer'].id - except KeyError: - pass - except AttributeError: - pass - try: - self.fields['scenario'].initial = cloned_kwargs['scenario'].id - except KeyError: - pass - except AttributeError: - pass diff --git a/src/booking/migrations/0001_initial.py b/src/booking/migrations/0001_initial.py index 6932dae..20415fe 100644 --- a/src/booking/migrations/0001_initial.py +++ b/src/booking/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2016-11-03 13:33 -from __future__ import unicode_literals +# Generated by Django 2.1 on 2018-09-14 14:48 from django.conf import settings from django.db import migrations, models @@ -12,8 +10,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('dashboard', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('account', '0001_initial'), + ('resource_inventory', '__first__'), ] operations = [ @@ -23,9 +22,17 @@ class Migration(migrations.Migration): ('id', models.AutoField(primary_key=True, serialize=False)), ('start', models.DateTimeField()), ('end', models.DateTimeField()), - ('jira_issue_id', models.IntegerField(null=True)), - ('jira_issue_status', models.CharField(max_length=50)), + ('reset', models.BooleanField(default=False)), + ('jira_issue_id', models.IntegerField(blank=True, null=True)), + ('jira_issue_status', models.CharField(blank=True, max_length=50)), ('purpose', models.CharField(max_length=300)), + ('ext_count', models.IntegerField(default=2)), + ('project', models.CharField(blank=True, default='', max_length=100, null=True)), + ('collaborators', models.ManyToManyField(related_name='collaborators', to=settings.AUTH_USER_MODEL)), + ('config_bundle', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ConfigBundle')), + ('lab', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='account.Lab')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owner', to=settings.AUTH_USER_MODEL)), + ('resource', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ResourceBundle')), ], options={ 'db_table': 'booking', @@ -38,6 +45,14 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=30)), ], ), + migrations.CreateModel( + name='Opsys', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100)), + ('sup_installers', models.ManyToManyField(blank=True, to='booking.Installer')), + ], + ), migrations.CreateModel( name='Scenario', fields=[ @@ -46,23 +61,8 @@ class Migration(migrations.Migration): ], ), migrations.AddField( - model_name='booking', - name='installer', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='booking.Installer'), - ), - migrations.AddField( - model_name='booking', - name='resource', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dashboard.Resource'), - ), - migrations.AddField( - model_name='booking', - name='scenario', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='booking.Scenario'), - ), - migrations.AddField( - model_name='booking', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name='installer', + name='sup_scenarios', + field=models.ManyToManyField(blank=True, to='booking.Scenario'), ), ] diff --git a/src/booking/migrations/0002_booking_changeid.py b/src/booking/migrations/0002_booking_changeid.py deleted file mode 100644 index 33af8fd..0000000 --- a/src/booking/migrations/0002_booking_changeid.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2017-12-13 15:06 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Opsys', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100)), - ], - ), - migrations.AddField( - model_name='booking', - name='opsys', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='booking.Opsys'), - ), - migrations.AddField( - model_name='booking', - name='changeid', - field=models.TextField(default='no change ID'), - ), - migrations.AlterField( - model_name='booking', - name='changeid', - field=models.TextField(blank=True, default='no change ID', null=True), - ), - ] diff --git a/src/booking/migrations/0003_auto_20180108_2024.py b/src/booking/migrations/0003_auto_20180108_2024.py deleted file mode 100644 index 93cecc2..0000000 --- a/src/booking/migrations/0003_auto_20180108_2024.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2018-01-08 20:24 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0002_booking_changeid'), - ] - - operations = [ - migrations.AddField( - model_name='booking', - name='reset', - field=models.BooleanField(default=False), - ), - migrations.AlterField( - model_name='booking', - name='changeid', - field=models.TextField(blank=True, default='initial', null=True), - ), - ] \ No newline at end of file diff --git a/src/booking/migrations/0004_booking_ext_count.py b/src/booking/migrations/0004_booking_ext_count.py deleted file mode 100644 index 6bcc3ce..0000000 --- a/src/booking/migrations/0004_booking_ext_count.py +++ /dev/null @@ -1,27 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Sawyer Bergeron and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0003_auto_20180108_2024'), - ] - - operations = [ - migrations.AddField( - model_name='booking', - name='ext_count', - field=models.IntegerField(default=2), - ), - ] diff --git a/src/booking/migrations/__init__.py b/src/booking/migrations/__init__.py index b5914ce..e69de29 100644 --- a/src/booking/migrations/__init__.py +++ b/src/booking/migrations/__init__.py @@ -1,10 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - diff --git a/src/booking/models.py b/src/booking/models.py index 8762d46..adfc28d 100644 --- a/src/booking/models.py +++ b/src/booking/models.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -8,67 +9,61 @@ ############################################################################## +from resource_inventory.models import ResourceBundle, ConfigBundle +from account.models import Lab from django.conf import settings from django.contrib.auth.models import User from django.db import models from jira import JIRA from jira import JIRAError -from django.utils.crypto import get_random_string -import hashlib +import resource_inventory.resource_manager -from dashboard.models import Resource - -class Installer(models.Model): +class Scenario(models.Model): id = models.AutoField(primary_key=True) - name = models.CharField(max_length=30) + name = models.CharField(max_length=300) def __str__(self): return self.name -class Scenario(models.Model): + +class Installer(models.Model): id = models.AutoField(primary_key=True) - name = models.CharField(max_length=300) + name = models.CharField(max_length=30) + sup_scenarios = models.ManyToManyField(Scenario, blank=True) def __str__(self): return self.name + class Opsys(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=100) + sup_installers = models.ManyToManyField(Installer, blank=True) def __str__(self): return self.name + class Booking(models.Model): id = models.AutoField(primary_key=True) - changeid = models.TextField(default='initial', blank=True, null=True) - user = models.ForeignKey(User, models.CASCADE) # delete if user is deleted - resource = models.ForeignKey(Resource, models.PROTECT) + owner = models.ForeignKey(User, models.CASCADE, related_name='owner') # delete if user is deleted + collaborators = models.ManyToManyField(User, related_name='collaborators') start = models.DateTimeField() end = models.DateTimeField() reset = models.BooleanField(default=False) - jira_issue_id = models.IntegerField(null=True) - jira_issue_status = models.CharField(max_length=50) - - opsys = models.ForeignKey(Opsys, models.DO_NOTHING, null=True) - installer = models.ForeignKey(Installer, models.DO_NOTHING, null=True) - scenario = models.ForeignKey(Scenario, models.DO_NOTHING, null=True) + jira_issue_id = models.IntegerField(null=True, blank=True) + jira_issue_status = models.CharField(max_length=50, blank=True) purpose = models.CharField(max_length=300, blank=False) ext_count = models.IntegerField(default=2) + resource = models.ForeignKey(ResourceBundle, on_delete=models.SET_NULL, null=True) #need to decide behavior here on delete + config_bundle = models.ForeignKey(ConfigBundle, on_delete=models.SET_NULL, null=True) + project = models.CharField(max_length=100, default="", blank=True, null=True) + lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL) class Meta: db_table = 'booking' - def get_jira_issue(self): - try: - jira = JIRA(server=settings.JIRA_URL, - basic_auth=(settings.JIRA_USER_NAME, settings.JIRA_USER_PASSWORD)) - issue = jira.issue(self.jira_issue_id) - return issue - except JIRAError: - return None - def save(self, *args, **kwargs): """ Save the booking if self.user is authorized and there is no overlapping booking. @@ -83,11 +78,14 @@ class Booking(models.Model): conflicting_dates = conflicting_dates.filter(start__lt=self.end) if conflicting_dates.count() > 0: raise ValueError('This booking overlaps with another booking') - if not self.changeid: - self.changeid = self.id - else: - self.changeid = hashlib.md5(self.changeid.encode() + get_random_string(length=32).encode()).hexdigest() return super(Booking, self).save(*args, **kwargs) + def delete(self, *args, **kwargs): + res = self.resource + self.resource = None + self.save() + resource_inventory.resource_manager.ResourceManager.getInstance().deleteResourceBundle(res) + return super(self.__class__, self).delete(*args, **kwargs) + def __str__(self): - return str(self.resource) + ' from ' + str(self.start) + ' until ' + str(self.end) + return str(self.purpose) + ' from ' + str(self.start) + ' until ' + str(self.end) diff --git a/src/booking/stats.py b/src/booking/stats.py new file mode 100644 index 0000000..31f7ef1 --- /dev/null +++ b/src/booking/stats.py @@ -0,0 +1,56 @@ +############################################################################## +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +from booking.models import Booking +import datetime +import pytz + + +class StatisticsManager(object): + + @staticmethod + def getContinuousBookingTimeSeries(span=28): + """ + Will return a dictionary of names and 2-D array of x and y data points. + e.g. {"plot1": [["x1", "x2", "x3"],["y1", "y2", "y3]]} + x values will be dates in string + every change (booking start / end) will be reflected, instead of one data point per day + y values are the integer number of bookings/users active at some point in the given date + span is the number of days to plot. The last x value will always be the current time + """ + x_set = set() + x = [] + y = [] + users = [] + now = datetime.datetime.now(pytz.utc) + delta = datetime.timedelta(days=span) + end = now-delta + bookings = Booking.objects.filter(start__lte=now, end__gte=end) + for booking in bookings: + x_set.add(booking.start) + if booking.end < now: + x_set.add(booking.end) + + x_set.add(now) + x_set.add(end) + + x_list = list(x_set) + x_list.sort(reverse=True) + for time in x_list: + x.append(str(time)) + active = Booking.objects.filter(start__lte=time, end__gt=time) + booking_count = len(active) + users_set = set() + for booking in active: + users_set.add(booking.owner) + for user in booking.collaborators.all(): + users_set.add(user) + y.append(booking_count) + users.append(len(users_set)) + + return {"booking": [x, y], "user": [x, users]} diff --git a/src/booking/tests/__init__.py b/src/booking/tests/__init__.py index b5914ce..b6fef6c 100644 --- a/src/booking/tests/__init__.py +++ b/src/booking/tests/__init__.py @@ -6,5 +6,3 @@ # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - - diff --git a/src/booking/tests/test_models.py b/src/booking/tests/test_models.py index b4cd113..a83f817 100644 --- a/src/booking/tests/test_models.py +++ b/src/booking/tests/test_models.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -15,29 +16,28 @@ from django.test import TestCase from django.utils import timezone from booking.models import * -from dashboard.models import Resource -from jenkins.models import JenkinsSlave - +from resource_inventory.models import ResourceBundle, GenericResourceBundle, ConfigBundle class BookingModelTestCase(TestCase): + count = 0 def setUp(self): - self.slave = JenkinsSlave.objects.create(name='test', url='test') self.owner = User.objects.create(username='owner') - self.res1 = Resource.objects.create(name='res1', slave=self.slave, description='x', - url='x',owner=self.owner) - self.res2 = Resource.objects.create(name='res2', slave=self.slave, description='x', - url='x',owner=self.owner) - + self.res1 = ResourceBundle.objects.create( + template=GenericResourceBundle.objects.create(name="gbundle" + str(self.count)) + ) + self.count += 1 + self.res2 = ResourceBundle.objects.create( + template=GenericResourceBundle.objects.create(name="gbundle2" + str(self.count)) + ) + self.count += 1 self.user1 = User.objects.create(username='user1') self.add_booking_perm = Permission.objects.get(codename='add_booking') self.user1.user_permissions.add(self.add_booking_perm) self.user1 = User.objects.get(pk=self.user1.id) - - self.installer = Installer.objects.create(name='TestInstaller') - self.scenario = Scenario.objects.create(name='TestScenario') + self.config_bundle = ConfigBundle.objects.create(owner=self.user1, name="test config") def test_start_end(self): """ @@ -46,11 +46,25 @@ class BookingModelTestCase(TestCase): """ start = timezone.now() end = start - timedelta(weeks=1) - self.assertRaises(ValueError, Booking.objects.create, start=start, end=end, - resource=self.res1, user=self.user1) + self.assertRaises( + ValueError, + Booking.objects.create, + start=start, + end=end, + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) end = start - self.assertRaises(ValueError, Booking.objects.create, start=start, end=end, - resource=self.res1, user=self.user1) + self.assertRaises( + ValueError, + Booking.objects.create, + start=start, + end=end, + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) def test_conflicts(self): """ @@ -60,35 +74,153 @@ class BookingModelTestCase(TestCase): start = timezone.now() end = start + timedelta(weeks=1) self.assertTrue( - Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1)) - - self.assertRaises(ValueError, Booking.objects.create, start=start, - end=end, resource=self.res1, user=self.user1) - self.assertRaises(ValueError, Booking.objects.create, start=start + timedelta(days=1), - end=end - timedelta(days=1), resource=self.res1, user=self.user1) + Booking.objects.create( + start=start, + end=end, + owner=self.user1, + resource=self.res1, + config_bundle=self.config_bundle + ) + ) + + self.assertRaises( + ValueError, + Booking.objects.create, + start=start, + end=end, + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) + self.assertRaises( + ValueError, + Booking.objects.create, + start=start + timedelta(days=1), + end=end - timedelta(days=1), + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) + + self.assertRaises( + ValueError, + Booking.objects.create, + start=start - timedelta(days=1), + end=end, + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) + + self.assertRaises( + ValueError, + Booking.objects.create, + start=start - timedelta(days=1), + end=end - timedelta(days=1), + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) + + self.assertRaises( + ValueError, + Booking.objects.create, + start=start, + end=end + timedelta(days=1), + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) + + self.assertRaises( + ValueError, + Booking.objects.create, + start=start + timedelta(days=1), + end=end + timedelta(days=1), + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) - self.assertRaises(ValueError, Booking.objects.create, start=start - timedelta(days=1), - end=end, resource=self.res1, user=self.user1) - self.assertRaises(ValueError, Booking.objects.create, start=start - timedelta(days=1), - end=end - timedelta(days=1), resource=self.res1, user=self.user1) - - self.assertRaises(ValueError, Booking.objects.create, start=start, - end=end + timedelta(days=1), resource=self.res1, user=self.user1) - self.assertRaises(ValueError, Booking.objects.create, start=start + timedelta(days=1), - end=end + timedelta(days=1), resource=self.res1, user=self.user1) + self.assertTrue( + Booking.objects.create( + start=start - timedelta(days=1), + end=start, + owner=self.user1, + resource=self.res1, + config_bundle=self.config_bundle + ) + ) + self.assertTrue( + Booking.objects.create( + start=end, + end=end + timedelta(days=1), + owner=self.user1, + resource=self.res1, + config_bundle=self.config_bundle + ) + ) - self.assertTrue(Booking.objects.create(start=start - timedelta(days=1), end=start, - user=self.user1, resource=self.res1)) - self.assertTrue(Booking.objects.create(start=end, end=end + timedelta(days=1), - user=self.user1, resource=self.res1)) + self.assertTrue( + Booking.objects.create( + start=start - timedelta(days=2), + end=start - timedelta(days=1), + owner=self.user1, + resource=self.res1, + config_bundle=self.config_bundle + ) + ) self.assertTrue( - Booking.objects.create(start=start - timedelta(days=2), end=start - timedelta(days=1), - user=self.user1, resource=self.res1)) + Booking.objects.create( + start=end + timedelta(days=1), + end=end + timedelta(days=2), + owner=self.user1, + resource=self.res1, + config_bundle=self.config_bundle + ) + ) + self.assertTrue( - Booking.objects.create(start=end + timedelta(days=1), end=end + timedelta(days=2), - user=self.user1, resource=self.res1)) + Booking.objects.create( + start=start, + end=end, + owner=self.user1, + resource=self.res2, + config_bundle=self.config_bundle + ) + ) + + def test_extensions(self): + """ + saving a booking with an extended end time is allows to happen twice, + and each extension must be a maximum of one week long + """ + start = timezone.now() + end = start + timedelta(weeks=1) self.assertTrue( - Booking.objects.create(start=start, end=end, - user=self.user1, resource=self.res2, scenario=self.scenario, - installer=self.installer)) \ No newline at end of file + Booking.objects.create( + start=start, + end=end, + owner=self.user1, + resource=self.res1, + config_bundle=self.config_bundle + ) + ) + + booking = Booking.objects.all().first() # should be only thing in db + + self.assertEquals(booking.ext_count, 2) + booking.end = booking.end + timedelta(days=3) + try: + booking.save() + except Exception: + self.fail("save() threw an exception") + booking.end = booking.end + timedelta(weeks=2) + self.assertRaises(ValueError, booking.save) + booking.end = booking.end - timedelta(days=8) + try: + self.assertTrue(booking.save()) + except Exception: + self.fail("save() threw an exception") + diff --git a/src/booking/tests/test_views.py b/src/booking/tests/test_views.py deleted file mode 100644 index c1da013..0000000 --- a/src/booking/tests/test_views.py +++ /dev/null @@ -1,106 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - -from datetime import timedelta - -from django.test import Client -from django.test import TestCase -from django.urls import reverse -from django.utils import timezone -from django.utils.encoding import force_text -from registration.forms import User - -from account.models import UserProfile -from booking.models import Booking -from dashboard.models import Resource -from jenkins.models import JenkinsSlave - - -class BookingViewTestCase(TestCase): - def setUp(self): - self.client = Client() - self.slave = JenkinsSlave.objects.create(name='test', url='test') - self.owner = User.objects.create(username='owner') - self.res1 = Resource.objects.create(name='res1', slave=self.slave, description='x', - url='x',owner=self.owner) - self.user1 = User.objects.create(username='user1') - self.user1.set_password('user1') - self.user1profile = UserProfile.objects.create(user=self.user1) - self.user1.save() - - self.user1 = User.objects.get(pk=self.user1.id) - - - def test_resource_bookings_json(self): - url = reverse('booking:bookings_json', kwargs={'resource_id': 0}) - self.assertEqual(self.client.get(url).status_code, 404) - - url = reverse('booking:bookings_json', kwargs={'resource_id': self.res1.id}) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertJSONEqual(force_text(response.content), {"bookings": []}) - booking1 = Booking.objects.create(start=timezone.now(), - end=timezone.now() + timedelta(weeks=1), user=self.user1, - resource=self.res1) - response = self.client.get(url) - json = response.json() - self.assertEqual(response.status_code, 200) - self.assertIn('bookings', json) - self.assertEqual(len(json['bookings']), 1) - self.assertIn('start', json['bookings'][0]) - self.assertIn('end', json['bookings'][0]) - self.assertIn('id', json['bookings'][0]) - self.assertIn('purpose', json['bookings'][0]) - - def test_booking_form_view(self): - url = reverse('booking:create', kwargs={'resource_id': 0}) - self.assertEqual(self.client.get(url).status_code, 404) - - # authenticated user - url = reverse('booking:create', kwargs={'resource_id': self.res1.id}) - self.client.login(username='user1',password='user1') - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed('booking/booking_calendar.html') - self.assertTemplateUsed('booking/booking_form.html') - self.assertIn('resource', response.context) - - - def test_booking_view(self): - start = timezone.now() - end = start + timedelta(weeks=1) - booking = Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1) - - url = reverse('booking:detail', kwargs={'booking_id':0}) - response = self.client.get(url) - self.assertEqual(response.status_code, 404) - - url = reverse('booking:detail', kwargs={'booking_id':booking.id}) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed('booking/booking_detail.html') - self.assertIn('booking', response.context) - - def test_booking_list_view(self): - start = timezone.now() - timedelta(weeks=2) - end = start + timedelta(weeks=1) - Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1) - - url = reverse('booking:list') - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed('booking/booking_list.html') - self.assertTrue(len(response.context['bookings']) == 0) - - start = timezone.now() - end = start + timedelta(weeks=1) - Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1) - response = self.client.get(url) - self.assertTrue(len(response.context['bookings']) == 1) \ No newline at end of file diff --git a/src/booking/urls.py b/src/booking/urls.py index ed3b1d4..88fbb0a 100644 --- a/src/booking/urls.py +++ b/src/booking/urls.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -27,20 +28,19 @@ from django.conf.urls import url from booking.views import * +app_name = "booking" urlpatterns = [ - url(r'^(?P[0-9]+)/$', BookingFormView.as_view(), name='create'), - url(r'^(?P[0-9]+)/edit/(?P[0-9]+)/$', BookingEditFormView.as_view(), name='edit'), - url(r'^(?P[0-9]+)/bookings_json/$', ResourceBookingsJSON.as_view(), - name='bookings_json'), - url(r'^detail/$', BookingView.as_view(), name='detail_prefix'), - url(r'^detail/(?P[0-9]+)/$', BookingView.as_view(), name='detail'), + url(r'^detail/(?P[0-9]+)/$', booking_detail_view, name='detail'), + url(r'^(?P[0-9]+)/$', booking_detail_view, name='booking_detail'), url(r'^delete/$', BookingDeleteView.as_view(), name='delete_prefix'), url(r'^delete/(?P[0-9]+)/$', BookingDeleteView.as_view(), name='delete'), url(r'^delete/(?P[0-9]+)/confirm/$', bookingDelete, name='delete_booking'), - url(r'^list/$', BookingListView.as_view(), name='list') -] \ No newline at end of file + url(r'^list/$', BookingListView.as_view(), name='list'), + url(r'^stats/$', booking_stats_view, name='stats'), + url(r'^stats/json$', booking_stats_json, name='stats_json'), +] diff --git a/src/booking/views.py b/src/booking/views.py index a52cfe2..c139b4c 100644 --- a/src/booking/views.py +++ b/src/booking/views.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -7,39 +8,38 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## -from django.conf import settings from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import JsonResponse from django.shortcuts import get_object_or_404 +from django.http import JsonResponse from django.urls import reverse from django.utils import timezone from django.views import View from django.views.generic import FormView from django.views.generic import TemplateView -from jira import JIRAError -from django.shortcuts import redirect +from django.shortcuts import redirect, render +import json -from account.jira_util import get_jira from booking.forms import BookingForm, BookingEditForm -from booking.models import Booking -from dashboard.models import Resource - -def create_jira_ticket(user, booking): - jira = get_jira(user) - issue_dict = { - 'project': 'PHAROS', - 'summary': str(booking.resource) + ': Access Request', - 'description': booking.purpose, - 'issuetype': {'name': 'Task'}, - 'components': [{'name': 'POD Access Request'}], - 'assignee': {'name': booking.resource.owner.username} - } - issue = jira.create_issue(fields=issue_dict) - jira.add_attachment(issue, user.userprofile.pgp_public_key) - jira.add_attachment(issue, user.userprofile.ssh_public_key) - booking.jira_issue_id = issue.id - booking.save() +from resource_inventory.models import ResourceBundle +from resource_inventory.resource_manager import ResourceManager +from booking.models import Booking, Installer, Opsys +from booking.stats import StatisticsManager + + +def drop_filter(context): + installer_filter = {} + for os in Opsys.objects.all(): + installer_filter[os.id] = [] + for installer in os.sup_installers.all(): + installer_filter[os.id].append(installer.id) + + scenario_filter = {} + for installer in Installer.objects.all(): + scenario_filter[installer.id] = [] + for scenario in installer.sup_scenarios.all(): + scenario_filter[installer.id].append(scenario.id) + + context.update({'installer_filter': json.dumps(installer_filter), 'scenario_filter': json.dumps(scenario_filter)}) class BookingFormView(FormView): @@ -47,14 +47,16 @@ class BookingFormView(FormView): form_class = BookingForm def dispatch(self, request, *args, **kwargs): - self.resource = get_object_or_404(Resource, id=self.kwargs['resource_id']) + self.resource = get_object_or_404(ResourceBundle, id=self.kwargs['resource_id']) return super(BookingFormView, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): - title = 'Booking: ' + self.resource.name + title = 'Booking: ' + str(self.resource.id) context = super(BookingFormView, self).get_context_data(**kwargs) context.update({'title': title, 'resource': self.resource}) - #raise PermissionDenied('check') + + drop_filter(context) + return context def get_success_url(self): @@ -75,24 +77,16 @@ class BookingFormView(FormView): booking = Booking(start=form.cleaned_data['start'], end=form.cleaned_data['end'], purpose=form.cleaned_data['purpose'], - opsys=form.cleaned_data['opsys'], installer=form.cleaned_data['installer'], scenario=form.cleaned_data['scenario'], - resource=self.resource, user=user) + resource=self.resource, + owner=user + ) try: booking.save() except ValueError as err: messages.add_message(self.request, messages.ERROR, err) return super(BookingFormView, self).form_invalid(form) - try: - if settings.CREATE_JIRA_TICKET: - create_jira_ticket(user, booking) - except JIRAError: - messages.add_message(self.request, messages.ERROR, 'Failed to create Jira Ticket. ' - 'Please check your Jira ' - 'permissions.') - booking.delete() - return super(BookingFormView, self).form_invalid(form) messages.add_message(self.request, messages.SUCCESS, 'Booking saved') return super(BookingFormView, self).form_valid(form) @@ -105,7 +99,7 @@ class BookingEditFormView(FormView): return True def dispatch(self, request, *args, **kwargs): - self.resource = get_object_or_404(Resource, id=self.kwargs['resource_id']) + self.resource = get_object_or_404(ResourceBundle, id=self.kwargs['resource_id']) self.original_booking = get_object_or_404(Booking, id=self.kwargs['booking_id']) return super(BookingEditFormView, self).dispatch(request, *args, **kwargs) @@ -113,6 +107,9 @@ class BookingEditFormView(FormView): title = 'Editing Booking on: ' + self.resource.name context = super(BookingEditFormView, self).get_context_data(**kwargs) context.update({'title': title, 'resource': self.resource, 'booking': self.original_booking}) + + drop_filter(context) + return context def get_form_kwargs(self): @@ -120,14 +117,6 @@ class BookingEditFormView(FormView): kwargs['purpose'] = self.original_booking.purpose kwargs['start'] = self.original_booking.start kwargs['end'] = self.original_booking.end - try: - kwargs['installer'] = self.original_booking.installer - except AttributeError: - pass - try: - kwargs['scenario'] = self.original_booking.scenario - except AttributeError: - pass return kwargs def get_success_url(self): @@ -145,7 +134,7 @@ class BookingEditFormView(FormView): 'You are not the owner of this booking.') return super(BookingEditFormView, self).form_invalid(form) - #Do Conflict Checks + # Do Conflict Checks if self.original_booking.end != form.cleaned_data['end']: if form.cleaned_data['end'] - self.original_booking.end > timezone.timedelta(days=7): messages.add_message(self.request, messages.ERROR, @@ -176,13 +165,12 @@ class BookingEditFormView(FormView): messages.add_message(self.request, messages.ERROR, err) return super(BookingEditFormView, self).form_invalid(form) - user = self.request.user return super(BookingEditFormView, self).form_valid(form) + class BookingView(TemplateView): template_name = "booking/booking_detail.html" - def get_context_data(self, **kwargs): booking = get_object_or_404(Booking, id=self.kwargs['booking_id']) title = 'Booking Details' @@ -190,6 +178,7 @@ class BookingView(TemplateView): context.update({'title': title, 'booking': booking}) return context + class BookingDeleteView(TemplateView): template_name = "booking/booking_delete.html" @@ -200,12 +189,14 @@ class BookingDeleteView(TemplateView): context.update({'title': title, 'booking': booking}) return context + def bookingDelete(request, booking_id): booking = get_object_or_404(Booking, id=booking_id) booking.delete() messages.add_message(request, messages.SUCCESS, 'Booking deleted') return redirect('../../../../') + class BookingListView(TemplateView): template_name = "booking/booking_list.html" @@ -219,8 +210,47 @@ class BookingListView(TemplateView): class ResourceBookingsJSON(View): def get(self, request, *args, **kwargs): - resource = get_object_or_404(Resource, id=self.kwargs['resource_id']) - bookings = resource.booking_set.get_queryset().values('id', 'start', 'end', 'purpose', - 'jira_issue_status', 'opsys__name', - 'installer__name', 'scenario__name') + resource = get_object_or_404(ResourceBundle, id=self.kwargs['resource_id']) + bookings = resource.booking_set.get_queryset().values( + 'id', + 'start', + 'end', + 'purpose', + 'jira_issue_status', + 'config_bundle__name' + ) return JsonResponse({'bookings': list(bookings)}) + + +def booking_detail_view(request, booking_id): + user = None + if request.user.is_authenticated: + user = request.user + else: + return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) + + booking = get_object_or_404(Booking, id=booking_id) + return render(request, "booking/booking_detail.html", { + 'title': 'Booking Details', + 'booking': booking, + 'pdf': ResourceManager().makePDF(booking.resource), + 'user_id': user.id}) + + +def booking_stats_view(request): + return render( + request, + "booking/stats.html", + context={ + "data": StatisticsManager.getContinuousBookingTimeSeries(), + "title": "Booking Statistics" + } + ) + + +def booking_stats_json(request): + try: + span = int(request.GET.get("days", 14)) + except: + span = 14 + return JsonResponse(StatisticsManager.getContinuousBookingTimeSeries(span), safe=False) -- cgit 1.2.3-korg