From c32479489db670904579a580a918f3513581fda9 Mon Sep 17 00:00:00 2001 From: Sawyer Bergeron Date: Fri, 5 Jan 2018 16:07:13 -0500 Subject: Implement Booking Modification Interface Jira: PHAROS-330 Users can change start date if it has not already occurred, and can change end date, purpose, and both installer and scenario. Standard checks apply similar to when initially creating a booking. Change-Id: Ibae7fe91a58bd6e0741db065265c05c3823bdc27 Signed-off-by: Sawyer Bergeron --- .../account/migrations/0002_auto_20180109_2002.py | 33 ++++++++++ .../migrations/0002_userprofile_email_addr.py | 20 ------ dashboard/src/account/migrations/0003_lab.py | 37 ----------- dashboard/src/api/serializers.py | 2 +- dashboard/src/booking/forms.py | 44 ++++++++++++- .../booking/migrations/0002_auto_20171213_1506.py | 28 -------- .../booking/migrations/0002_booking_changeid.py | 38 +++++++++++ .../booking/migrations/0003_auto_20180108_2024.py | 25 ++++++++ dashboard/src/booking/models.py | 8 +++ dashboard/src/booking/urls.py | 1 + dashboard/src/booking/views.py | 74 +++++++++++++++++++++- dashboard/src/notifier/migrations/0001_initial.py | 1 - dashboard/src/static/js/booking-calendar.js | 15 +++++ dashboard/src/static/js/fullcalendar-options.js | 2 +- .../src/templates/booking/booking_calendar.html | 22 +++++++ .../src/templates/booking/booking_detail.html | 9 +++ 16 files changed, 266 insertions(+), 93 deletions(-) create mode 100644 dashboard/src/account/migrations/0002_auto_20180109_2002.py delete mode 100644 dashboard/src/account/migrations/0002_userprofile_email_addr.py delete mode 100644 dashboard/src/account/migrations/0003_lab.py delete mode 100644 dashboard/src/booking/migrations/0002_auto_20171213_1506.py create mode 100644 dashboard/src/booking/migrations/0002_booking_changeid.py create mode 100644 dashboard/src/booking/migrations/0003_auto_20180108_2024.py (limited to 'dashboard') diff --git a/dashboard/src/account/migrations/0002_auto_20180109_2002.py b/dashboard/src/account/migrations/0002_auto_20180109_2002.py new file mode 100644 index 0000000..5cf8a70 --- /dev/null +++ b/dashboard/src/account/migrations/0002_auto_20180109_2002.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-01-09 20:02 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('account', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Lab', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200, unique=True)), + ('contact_email', models.EmailField(blank=True, max_length=200, null=True)), + ('contact_phone', models.CharField(blank=True, max_length=20, null=True)), + ('lab_user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='userprofile', + name='email_addr', + field=models.CharField(default='email@mail.com', max_length=300), + ), + ] diff --git a/dashboard/src/account/migrations/0002_userprofile_email_addr.py b/dashboard/src/account/migrations/0002_userprofile_email_addr.py deleted file mode 100644 index bfbed17..0000000 --- a/dashboard/src/account/migrations/0002_userprofile_email_addr.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2017-12-14 20:37 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('account', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='userprofile', - name='email_addr', - field=models.CharField(default='email@mail.com', max_length=300), - ), - ] diff --git a/dashboard/src/account/migrations/0003_lab.py b/dashboard/src/account/migrations/0003_lab.py deleted file mode 100644 index c4643c5..0000000 --- a/dashboard/src/account/migrations/0003_lab.py +++ /dev/null @@ -1,37 +0,0 @@ -############################################################################## -# Copyright (c) 2016 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 -############################################################################## - -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2018-01-09 15:34 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('account', '0002_userprofile_email_addr'), - ] - - operations = [ - migrations.CreateModel( - name='Lab', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=200, unique=True)), - ('contact_email', models.EmailField(blank=True, max_length=200, null=True)), - ('contact_phone', models.CharField(blank=True, max_length=20, null=True)), - ('lab_user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/dashboard/src/api/serializers.py b/dashboard/src/api/serializers.py index c371a92..794e139 100644 --- a/dashboard/src/api/serializers.py +++ b/dashboard/src/api/serializers.py @@ -21,7 +21,7 @@ class BookingSerializer(serializers.ModelSerializer): class Meta: model = Booking - fields = ('id', 'resource_id', 'start', 'end', 'opsys_name', 'installer_name', 'scenario_name', 'purpose') + fields = ('id', 'changeid', 'reset', 'resource_id', 'opsys_name', 'start', 'end', 'installer_name', 'scenario_name', 'purpose') class ServerSerializer(serializers.ModelSerializer): diff --git a/dashboard/src/booking/forms.py b/dashboard/src/booking/forms.py index 1f09c05..b4acd8f 100644 --- a/dashboard/src/booking/forms.py +++ b/dashboard/src/booking/forms.py @@ -11,10 +11,21 @@ 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', 'installer', 'scenario'] + 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) + 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() @@ -22,3 +33,32 @@ class BookingForm(forms.Form): 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/dashboard/src/booking/migrations/0002_auto_20171213_1506.py b/dashboard/src/booking/migrations/0002_auto_20171213_1506.py deleted file mode 100644 index 3e0a5fa..0000000 --- a/dashboard/src/booking/migrations/0002_auto_20171213_1506.py +++ /dev/null @@ -1,28 +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'), - ), - ] diff --git a/dashboard/src/booking/migrations/0002_booking_changeid.py b/dashboard/src/booking/migrations/0002_booking_changeid.py new file mode 100644 index 0000000..33af8fd --- /dev/null +++ b/dashboard/src/booking/migrations/0002_booking_changeid.py @@ -0,0 +1,38 @@ +# -*- 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/dashboard/src/booking/migrations/0003_auto_20180108_2024.py b/dashboard/src/booking/migrations/0003_auto_20180108_2024.py new file mode 100644 index 0000000..93cecc2 --- /dev/null +++ b/dashboard/src/booking/migrations/0003_auto_20180108_2024.py @@ -0,0 +1,25 @@ +# -*- 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/dashboard/src/booking/models.py b/dashboard/src/booking/models.py index 0bf5961..9156484 100644 --- a/dashboard/src/booking/models.py +++ b/dashboard/src/booking/models.py @@ -13,6 +13,8 @@ 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 from dashboard.models import Resource @@ -40,10 +42,12 @@ class Opsys(models.Model): 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) 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) @@ -78,6 +82,10 @@ 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 __str__(self): diff --git a/dashboard/src/booking/urls.py b/dashboard/src/booking/urls.py index 9e01316..403e32f 100644 --- a/dashboard/src/booking/urls.py +++ b/dashboard/src/booking/urls.py @@ -29,6 +29,7 @@ from booking.views import * 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'), diff --git a/dashboard/src/booking/views.py b/dashboard/src/booking/views.py index da2fe11..ab3a04e 100644 --- a/dashboard/src/booking/views.py +++ b/dashboard/src/booking/views.py @@ -7,7 +7,6 @@ # 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 @@ -21,11 +20,10 @@ from django.views.generic import TemplateView from jira import JIRAError from account.jira_util import get_jira -from booking.forms import BookingForm +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 = { @@ -55,6 +53,7 @@ class BookingFormView(FormView): title = 'Booking: ' + self.resource.name context = super(BookingFormView, self).get_context_data(**kwargs) context.update({'title': title, 'resource': self.resource}) + #raise PermissionDenied('check') return context def get_success_url(self): @@ -92,6 +91,75 @@ class BookingFormView(FormView): return super(BookingFormView, self).form_valid(form) +class BookingEditFormView(FormView): + template_name = "booking/booking_calendar.html" + form_class = BookingEditForm + + def is_valid(self): + return True + + def dispatch(self, request, *args, **kwargs): + self.resource = get_object_or_404(Resource, 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) + + def get_context_data(self, **kwargs): + title = 'Editing Booking on: ' + self.resource.name + context = super(BookingEditFormView, self).get_context_data(**kwargs) + context.update({'title': title, 'resource': self.resource}) + return context + + def get_form_kwargs(self): + kwargs = super(BookingEditFormView, self).get_form_kwargs() + 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): + return reverse('booking:create', args=(self.resource.id,)) + + def form_valid(self, form): + + if not self.request.user.is_authenticated: + messages.add_message(self.request, messages.ERROR, + 'You need to be logged in to book a Pod.') + return super(BookingEditFormView, self).form_invalid(form) + + if not self.request.user == self.original_booking.user: + messages.add_message(self.request, messages.ERROR, + 'You are not the owner of this booking.') + return super(BookingEditFormView, self).form_invalid(form) + + #Do Conflict Checks + if self.original_booking.start != form.cleaned_data['start']: + if timezone.now() > form.cleaned_data['start']: + messages.add_message(self.request, messages.ERROR, + 'Cannot change start date after it has occurred.') + return super(BookingEditFormView, self).form_invalid(form) + self.original_booking.start = form.cleaned_data['start'] + self.original_booking.end = form.cleaned_data['end'] + self.original_booking.purpose = form.cleaned_data['purpose'] + self.original_booking.installer = form.cleaned_data['installer'] + self.original_booking.scenario = form.cleaned_data['scenario'] + self.original_booking.reset = form.cleaned_data['reset'] + try: + self.original_booking.save() + except ValueError as err: + 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" diff --git a/dashboard/src/notifier/migrations/0001_initial.py b/dashboard/src/notifier/migrations/0001_initial.py index 78d2bad..cac4d04 100644 --- a/dashboard/src/notifier/migrations/0001_initial.py +++ b/dashboard/src/notifier/migrations/0001_initial.py @@ -12,7 +12,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('account', '0002_userprofile_email_addr'), ('dashboard', '0002_auto_20170505_0815'), ] diff --git a/dashboard/src/static/js/booking-calendar.js b/dashboard/src/static/js/booking-calendar.js index f634293..303a6b2 100644 --- a/dashboard/src/static/js/booking-calendar.js +++ b/dashboard/src/static/js/booking-calendar.js @@ -51,8 +51,23 @@ function loadEvents(url) { } $(document).ready(function () { + createEditViewSwitch(); $('#calendar').fullCalendar(calendarOptions); loadEvents(bookings_url); $('#starttimepicker').datetimepicker(timepickerOptions); $('#endtimepicker').datetimepicker(timepickerOptions); + $('#starttimeeditpicker').datetimepicker(timepickerOptions); + $('#endtimeeditpicker').datetimepicker(timepickerOptions); }); + +function createEditViewSwitch() { + var url = window.location.href; + var isEdit = url.substr(url.lastIndexOf('/')); + + if ( url.indexOf('edit') !== -1 ) { + document.getElementById('booking_form_div').style.display = 'none'; + calendarOptions.selectable = false; + } else { + document.getElementById('booking_edit_form_div').style.display = 'none'; + } +} diff --git a/dashboard/src/static/js/fullcalendar-options.js b/dashboard/src/static/js/fullcalendar-options.js index 22a1b95..a29103a 100644 --- a/dashboard/src/static/js/fullcalendar-options.js +++ b/dashboard/src/static/js/fullcalendar-options.js @@ -98,4 +98,4 @@ var calendarOptions = { eventResize: function (event) { sendEventToForm(event); } -}; \ No newline at end of file +}; diff --git a/dashboard/src/templates/booking/booking_calendar.html b/dashboard/src/templates/booking/booking_calendar.html index 16f0a4a..52193d5 100644 --- a/dashboard/src/templates/booking/booking_calendar.html +++ b/dashboard/src/templates/booking/booking_calendar.html @@ -56,6 +56,28 @@ {% endbuttons %} +
+ {% bootstrap_form_errors form type='non_fields' %} +
+ {% csrf_token %} + +
+ {% bootstrap_field form.start addon_after='' %} +
+
+ {% bootstrap_field form.end addon_after='' %} +
+ {% bootstrap_field form.purpose %} + {% bootstrap_field form.installer %} + {% bootstrap_field form.scenario %} + {% bootstrap_field form.reset %} + {% buttons %} + + {% endbuttons %} +
+
{% else %}

Please diff --git a/dashboard/src/templates/booking/booking_detail.html b/dashboard/src/templates/booking/booking_detail.html index cb937d3..dd0bf03 100644 --- a/dashboard/src/templates/booking/booking_detail.html +++ b/dashboard/src/templates/booking/booking_detail.html @@ -27,3 +27,12 @@

Scenario: {{ booking.scenario }}

+{% if user.is_authenticated %} +{% if user.get_username == booking.user.username %} +

+ + Edit Booking + +

+{% endif %} +{% endif %} -- cgit 1.2.3-korg