summaryrefslogtreecommitdiffstats
path: root/tools/pharos-dashboard/src/booking
diff options
context:
space:
mode:
authormaxbr <maxbr@mi.fu-berlin.de>2016-09-26 16:36:11 +0200
committermaxbr <maxbr@mi.fu-berlin.de>2016-09-26 16:36:11 +0200
commitff30b14a212f38cf59084d30e9f13f9f92d2be3b (patch)
treecbe10ef00ef2be5650bedb5b81e24bd3b205f52c /tools/pharos-dashboard/src/booking
parent3ebfdcbcc91202482edb3f63fedde48646728fcd (diff)
Restructure dashboard project for docker deploying
Change-Id: I13cad51270504ee4bed8558598a8891af58a26ab Signed-off-by: maxbr <maxbr@mi.fu-berlin.de>
Diffstat (limited to 'tools/pharos-dashboard/src/booking')
-rw-r--r--tools/pharos-dashboard/src/booking/__init__.py10
-rw-r--r--tools/pharos-dashboard/src/booking/admin.py15
-rw-r--r--tools/pharos-dashboard/src/booking/apps.py15
-rw-r--r--tools/pharos-dashboard/src/booking/forms.py19
-rw-r--r--tools/pharos-dashboard/src/booking/migrations/__init__.py10
-rw-r--r--tools/pharos-dashboard/src/booking/models.py75
-rw-r--r--tools/pharos-dashboard/src/booking/tests/__init__.py10
-rw-r--r--tools/pharos-dashboard/src/booking/tests/test_models.py107
-rw-r--r--tools/pharos-dashboard/src/booking/tests/test_views.py81
-rw-r--r--tools/pharos-dashboard/src/booking/urls.py37
-rw-r--r--tools/pharos-dashboard/src/booking/views.py113
11 files changed, 492 insertions, 0 deletions
diff --git a/tools/pharos-dashboard/src/booking/__init__.py b/tools/pharos-dashboard/src/booking/__init__.py
new file mode 100644
index 00000000..b5914ce7
--- /dev/null
+++ b/tools/pharos-dashboard/src/booking/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# 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/tools/pharos-dashboard/src/booking/admin.py b/tools/pharos-dashboard/src/booking/admin.py
new file mode 100644
index 00000000..7a7f251a
--- /dev/null
+++ b/tools/pharos-dashboard/src/booking/admin.py
@@ -0,0 +1,15 @@
+##############################################################################
+# 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 django.contrib import admin
+
+from booking.models import Booking
+
+admin.site.register(Booking) \ No newline at end of file
diff --git a/tools/pharos-dashboard/src/booking/apps.py b/tools/pharos-dashboard/src/booking/apps.py
new file mode 100644
index 00000000..99bf115f
--- /dev/null
+++ b/tools/pharos-dashboard/src/booking/apps.py
@@ -0,0 +1,15 @@
+##############################################################################
+# 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 django.apps import AppConfig
+
+
+class BookingConfig(AppConfig):
+ name = 'booking'
diff --git a/tools/pharos-dashboard/src/booking/forms.py b/tools/pharos-dashboard/src/booking/forms.py
new file mode 100644
index 00000000..02ac887a
--- /dev/null
+++ b/tools/pharos-dashboard/src/booking/forms.py
@@ -0,0 +1,19 @@
+##############################################################################
+# 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
+
+
+class BookingForm(forms.Form):
+ fields = ['start', 'end', 'purpose']
+
+ start = forms.DateTimeField()
+ end = forms.DateTimeField()
+ purpose = forms.CharField(max_length=300) \ No newline at end of file
diff --git a/tools/pharos-dashboard/src/booking/migrations/__init__.py b/tools/pharos-dashboard/src/booking/migrations/__init__.py
new file mode 100644
index 00000000..b5914ce7
--- /dev/null
+++ b/tools/pharos-dashboard/src/booking/migrations/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# 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/tools/pharos-dashboard/src/booking/models.py b/tools/pharos-dashboard/src/booking/models.py
new file mode 100644
index 00000000..200dc830
--- /dev/null
+++ b/tools/pharos-dashboard/src/booking/models.py
@@ -0,0 +1,75 @@
+##############################################################################
+# 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 django.contrib.auth.models import User
+from django.db import models
+from jira import JIRA
+from jira import JIRAError
+
+from dashboard.models import Resource
+from django.conf import settings
+
+
+class Booking(models.Model):
+ id = models.AutoField(primary_key=True)
+ user = models.ForeignKey(User, models.CASCADE) # delete if user is deleted
+ resource = models.ForeignKey(Resource, models.PROTECT)
+ start = models.DateTimeField()
+ end = models.DateTimeField()
+ jira_issue_id = models.IntegerField(null=True)
+ jira_issue_status = models.CharField(max_length=50)
+
+ purpose = models.CharField(max_length=300, blank=False)
+
+ 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 authorization_test(self):
+ """
+ Return True if self.user is authorized to make this booking.
+ """
+ user = self.user
+ # Check if User is troubleshooter / admin
+ if user.has_perm('booking.add_booking'):
+ return True
+ # Check if User owns this resource
+ if user == self.resource.owner:
+ return True
+ return False
+
+ def save(self, *args, **kwargs):
+ """
+ Save the booking if self.user is authorized and there is no overlapping booking.
+ Raise PermissionError if the user is not authorized
+ Raise ValueError if there is an overlapping booking
+ """
+ if not self.authorization_test():
+ raise PermissionError('Insufficient permissions to save this booking.')
+ if self.start >= self.end:
+ raise ValueError('Start date is after end date')
+ # conflicts end after booking starts, and start before booking ends
+ conflicting_dates = Booking.objects.filter(resource=self.resource).exclude(id=self.id)
+ conflicting_dates = conflicting_dates.filter(end__gt=self.start)
+ conflicting_dates = conflicting_dates.filter(start__lt=self.end)
+ if conflicting_dates.count() > 0:
+ raise ValueError('This booking overlaps with another booking')
+ return super(Booking, self).save(*args, **kwargs)
+
+ def __str__(self):
+ return str(self.resource) + ' from ' + str(self.start) + ' until ' + str(self.end)
diff --git a/tools/pharos-dashboard/src/booking/tests/__init__.py b/tools/pharos-dashboard/src/booking/tests/__init__.py
new file mode 100644
index 00000000..b5914ce7
--- /dev/null
+++ b/tools/pharos-dashboard/src/booking/tests/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# 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/tools/pharos-dashboard/src/booking/tests/test_models.py b/tools/pharos-dashboard/src/booking/tests/test_models.py
new file mode 100644
index 00000000..612b35c0
--- /dev/null
+++ b/tools/pharos-dashboard/src/booking/tests/test_models.py
@@ -0,0 +1,107 @@
+##############################################################################
+# 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.contrib.auth.models import User, Permission
+from django.test import TestCase
+from django.utils import timezone
+
+from account.models import UserProfile
+from booking.models import Booking
+from dashboard.models import Resource
+from jenkins.models import JenkinsSlave
+
+
+class BookingModelTestCase(TestCase):
+ 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.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)
+
+ def test_start__end(self):
+ """
+ if the start of a booking is greater or equal then the end, saving should raise a
+ ValueException
+ """
+ start = timezone.now()
+ end = start - timedelta(weeks=1)
+ self.assertRaises(ValueError, Booking.objects.create, start=start, end=end,
+ resource=self.res1, user=self.user1)
+ end = start
+ self.assertRaises(ValueError, Booking.objects.create, start=start, end=end,
+ resource=self.res1, user=self.user1)
+
+ def test_conflicts(self):
+ """
+ saving an overlapping booking on the same resource should raise a ValueException
+ saving for different resources should succeed
+ """
+ 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)
+
+ 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,
+ 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),
+ user=self.user1, resource=self.res1))
+ self.assertTrue(
+ Booking.objects.create(start=end + timedelta(days=1), end=end + timedelta(days=2),
+ user=self.user1, resource=self.res1))
+ self.assertTrue(
+ Booking.objects.create(start=start, end=end,
+ user=self.user1, resource=self.res2))
+
+ def test_authorization(self):
+ user = User.objects.create(username='user')
+ user.userprofile = UserProfile.objects.create(user=user)
+ self.assertRaises(PermissionError, Booking.objects.create, start=timezone.now(),
+ end=timezone.now() + timedelta(days=1), resource=self.res1, user=user)
+ self.res1.owner = user
+ self.assertTrue(
+ Booking.objects.create(start=timezone.now(), end=timezone.now() + timedelta(days=1),
+ resource=self.res1, user=user))
+ self.res1.owner = self.owner
+ user.user_permissions.add(self.add_booking_perm)
+ user = User.objects.get(pk=user.id)
+ self.assertTrue(
+ Booking.objects.create(start=timezone.now(), end=timezone.now() + timedelta(days=1),
+ resource=self.res2, user=user))
diff --git a/tools/pharos-dashboard/src/booking/tests/test_views.py b/tools/pharos-dashboard/src/booking/tests/test_views.py
new file mode 100644
index 00000000..e568c155
--- /dev/null
+++ b/tools/pharos-dashboard/src/booking/tests/test_views.py
@@ -0,0 +1,81 @@
+##############################################################################
+# 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.contrib import auth
+from django.test import Client
+from django.utils import timezone
+from django.contrib.auth.models import Permission
+from django.test import TestCase
+from django.urls import reverse
+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.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)
+
+
+ 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)
+
+
+
diff --git a/tools/pharos-dashboard/src/booking/urls.py b/tools/pharos-dashboard/src/booking/urls.py
new file mode 100644
index 00000000..53206233
--- /dev/null
+++ b/tools/pharos-dashboard/src/booking/urls.py
@@ -0,0 +1,37 @@
+##############################################################################
+# 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
+##############################################################################
+
+
+"""pharos_dashboard URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/1.10/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.conf.urls import url, include
+ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
+"""
+from django.conf.urls import url
+
+from booking.views import *
+
+urlpatterns = [
+ url(r'^(?P<resource_id>[0-9]+)/$', BookingFormView.as_view(), name='create'),
+ url(r'^(?P<resource_id>[0-9]+)/bookings_json/$', ResourceBookingsJSON.as_view(),
+ name='bookings_json'),
+
+ url(r'^detail/$', BookingView.as_view(), name='detail_prefix'),
+ url(r'^detail/(?P<booking_id>[0-9]+)/$', BookingView.as_view(), name='detail'),
+]
diff --git a/tools/pharos-dashboard/src/booking/views.py b/tools/pharos-dashboard/src/booking/views.py
new file mode 100644
index 00000000..2fe167af
--- /dev/null
+++ b/tools/pharos-dashboard/src/booking/views.py
@@ -0,0 +1,113 @@
+##############################################################################
+# 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.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.shortcuts import redirect
+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 account.jira_util import get_jira
+from booking.forms import BookingForm
+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()
+
+
+class BookingFormView(LoginRequiredMixin, FormView):
+ template_name = "booking/booking_calendar.html"
+ form_class = BookingForm
+
+ def dispatch(self, request, *args, **kwargs):
+ self.resource = get_object_or_404(Resource, id=self.kwargs['resource_id'])
+ return super(BookingFormView, self).dispatch(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ title = 'Booking: ' + self.resource.name
+ context = super(BookingFormView, self).get_context_data(**kwargs)
+ context.update({'title': title, 'resource': self.resource})
+ return context
+
+ def get_success_url(self):
+ return reverse('booking:create', kwargs=self.kwargs)
+
+ def form_valid(self, form):
+ user = self.request.user
+ if not user.userprofile.ssh_public_key or not user.userprofile.pgp_public_key:
+ messages.add_message(self.request, messages.INFO,
+ 'Please upload your private keys before booking')
+ return redirect('account:settings')
+ booking = Booking(start=form.cleaned_data['start'], end=form.cleaned_data['end'],
+ purpose=form.cleaned_data['purpose'], resource=self.resource,
+ user=user)
+ try:
+ booking.save()
+ except ValueError as err:
+ messages.add_message(self.request, messages.ERROR, err)
+ return super(BookingFormView, self).form_invalid(form)
+ except PermissionError 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)
+
+
+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'])
+ jira_issue = booking.get_jira_issue()
+ title = 'Booking Details'
+ context = super(BookingView, self).get_context_data(**kwargs)
+ context.update({'title': title, 'booking': booking, 'jira_issue': jira_issue})
+ return context
+
+
+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')
+ return JsonResponse({'bookings': list(bookings)}) \ No newline at end of file