From ff30b14a212f38cf59084d30e9f13f9f92d2be3b Mon Sep 17 00:00:00 2001 From: maxbr Date: Mon, 26 Sep 2016 16:36:11 +0200 Subject: Restructure dashboard project for docker deploying Change-Id: I13cad51270504ee4bed8558598a8891af58a26ab Signed-off-by: maxbr --- tools/pharos-dashboard/src/booking/__init__.py | 10 ++ tools/pharos-dashboard/src/booking/admin.py | 15 +++ tools/pharos-dashboard/src/booking/apps.py | 15 +++ tools/pharos-dashboard/src/booking/forms.py | 19 ++++ .../src/booking/migrations/__init__.py | 10 ++ tools/pharos-dashboard/src/booking/models.py | 75 ++++++++++++++ .../pharos-dashboard/src/booking/tests/__init__.py | 10 ++ .../src/booking/tests/test_models.py | 107 +++++++++++++++++++ .../src/booking/tests/test_views.py | 81 +++++++++++++++ tools/pharos-dashboard/src/booking/urls.py | 37 +++++++ tools/pharos-dashboard/src/booking/views.py | 113 +++++++++++++++++++++ 11 files changed, 492 insertions(+) create mode 100644 tools/pharos-dashboard/src/booking/__init__.py create mode 100644 tools/pharos-dashboard/src/booking/admin.py create mode 100644 tools/pharos-dashboard/src/booking/apps.py create mode 100644 tools/pharos-dashboard/src/booking/forms.py create mode 100644 tools/pharos-dashboard/src/booking/migrations/__init__.py create mode 100644 tools/pharos-dashboard/src/booking/models.py create mode 100644 tools/pharos-dashboard/src/booking/tests/__init__.py create mode 100644 tools/pharos-dashboard/src/booking/tests/test_models.py create mode 100644 tools/pharos-dashboard/src/booking/tests/test_views.py create mode 100644 tools/pharos-dashboard/src/booking/urls.py create mode 100644 tools/pharos-dashboard/src/booking/views.py (limited to 'tools/pharos-dashboard/src/booking') 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[0-9]+)/$', BookingFormView.as_view(), name='create'), + 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'), +] 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 -- cgit 1.2.3-korg