diff options
Diffstat (limited to 'tools/pharos-dashboard/booking')
-rw-r--r-- | tools/pharos-dashboard/booking/__init__.py | 0 | ||||
-rw-r--r-- | tools/pharos-dashboard/booking/admin.py | 5 | ||||
-rw-r--r-- | tools/pharos-dashboard/booking/apps.py | 5 | ||||
-rw-r--r-- | tools/pharos-dashboard/booking/forms.py | 9 | ||||
-rw-r--r-- | tools/pharos-dashboard/booking/migrations/0001_initial.py | 36 | ||||
-rw-r--r-- | tools/pharos-dashboard/booking/migrations/0002_remove_booking_deleted.py | 19 | ||||
-rw-r--r-- | tools/pharos-dashboard/booking/migrations/0003_remove_booking_status.py | 19 | ||||
-rw-r--r-- | tools/pharos-dashboard/booking/migrations/__init__.py | 0 | ||||
-rw-r--r-- | tools/pharos-dashboard/booking/models.py | 53 | ||||
-rw-r--r-- | tools/pharos-dashboard/booking/tests/__init__.py | 0 | ||||
-rw-r--r-- | tools/pharos-dashboard/booking/tests/test_models.py | 88 | ||||
-rw-r--r-- | tools/pharos-dashboard/booking/tests/test_views.py | 72 | ||||
-rw-r--r-- | tools/pharos-dashboard/booking/urls.py | 24 | ||||
-rw-r--r-- | tools/pharos-dashboard/booking/views.py | 50 |
14 files changed, 380 insertions, 0 deletions
diff --git a/tools/pharos-dashboard/booking/__init__.py b/tools/pharos-dashboard/booking/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tools/pharos-dashboard/booking/__init__.py diff --git a/tools/pharos-dashboard/booking/admin.py b/tools/pharos-dashboard/booking/admin.py new file mode 100644 index 00000000..6055bed9 --- /dev/null +++ b/tools/pharos-dashboard/booking/admin.py @@ -0,0 +1,5 @@ +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/booking/apps.py b/tools/pharos-dashboard/booking/apps.py new file mode 100644 index 00000000..2d5f36f2 --- /dev/null +++ b/tools/pharos-dashboard/booking/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class BookingConfig(AppConfig): + name = 'booking' diff --git a/tools/pharos-dashboard/booking/forms.py b/tools/pharos-dashboard/booking/forms.py new file mode 100644 index 00000000..5b32c868 --- /dev/null +++ b/tools/pharos-dashboard/booking/forms.py @@ -0,0 +1,9 @@ +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/booking/migrations/0001_initial.py b/tools/pharos-dashboard/booking/migrations/0001_initial.py new file mode 100644 index 00000000..57735eef --- /dev/null +++ b/tools/pharos-dashboard/booking/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-08-12 09:51 +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): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('dashboard', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Booking', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('deleted', models.BooleanField(default=False)), + ('start', models.DateTimeField()), + ('end', models.DateTimeField()), + ('status', models.CharField(max_length=20)), + ('purpose', models.CharField(max_length=300)), + ('resource', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dashboard.Resource')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'booking', + }, + ), + ] diff --git a/tools/pharos-dashboard/booking/migrations/0002_remove_booking_deleted.py b/tools/pharos-dashboard/booking/migrations/0002_remove_booking_deleted.py new file mode 100644 index 00000000..335379d5 --- /dev/null +++ b/tools/pharos-dashboard/booking/migrations/0002_remove_booking_deleted.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-08-13 12:50 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('booking', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='booking', + name='deleted', + ), + ] diff --git a/tools/pharos-dashboard/booking/migrations/0003_remove_booking_status.py b/tools/pharos-dashboard/booking/migrations/0003_remove_booking_status.py new file mode 100644 index 00000000..95089a7a --- /dev/null +++ b/tools/pharos-dashboard/booking/migrations/0003_remove_booking_status.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-08-13 12:51 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('booking', '0002_remove_booking_deleted'), + ] + + operations = [ + migrations.RemoveField( + model_name='booking', + name='status', + ), + ] diff --git a/tools/pharos-dashboard/booking/migrations/__init__.py b/tools/pharos-dashboard/booking/migrations/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tools/pharos-dashboard/booking/migrations/__init__.py diff --git a/tools/pharos-dashboard/booking/models.py b/tools/pharos-dashboard/booking/models.py new file mode 100644 index 00000000..719dd9bf --- /dev/null +++ b/tools/pharos-dashboard/booking/models.py @@ -0,0 +1,53 @@ +from django.contrib.auth.models import User +from django.db import models + +from dashboard.models import Resource + + +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() + + purpose = models.CharField(max_length=300, blank=False) + + class Meta: + db_table = 'booking' + + 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 in self.resource.owners.all(): + 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) + 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/booking/tests/__init__.py b/tools/pharos-dashboard/booking/tests/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tools/pharos-dashboard/booking/tests/__init__.py diff --git a/tools/pharos-dashboard/booking/tests/test_models.py b/tools/pharos-dashboard/booking/tests/test_models.py new file mode 100644 index 00000000..e933f6e8 --- /dev/null +++ b/tools/pharos-dashboard/booking/tests/test_models.py @@ -0,0 +1,88 @@ +from datetime import timedelta + +from django.contrib.auth.models import User, Permission +from django.test import TestCase +from django.utils import timezone + +from booking.models import Booking +from dashboard.models import Resource + + +class BookingModelTestCase(TestCase): + def setUp(self): + self.res1 = Resource.objects.create(name='res1', slavename='s1', description='x', url='x') + self.res2 = Resource.objects.create(name='res2', slavename='s2', description='x', url='x') + + 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') + self.assertRaises(PermissionError, Booking.objects.create, start=timezone.now(), + end=timezone.now() + timedelta(days=1), resource=self.res1, user=user) + self.res1.owners.add(user) + self.assertTrue( + Booking.objects.create(start=timezone.now(), end=timezone.now() + timedelta(days=1), + resource=self.res1, user=user)) + 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/booking/tests/test_views.py b/tools/pharos-dashboard/booking/tests/test_views.py new file mode 100644 index 00000000..f5b75d14 --- /dev/null +++ b/tools/pharos-dashboard/booking/tests/test_views.py @@ -0,0 +1,72 @@ +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 + + +class BookingViewTestCase(TestCase): + def setUp(self): + self.client = Client() + self.res1 = Resource.objects.create(name='res1', slavename='s1', description='x', url='x') + 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) + + # anonymous user + url = reverse('booking:create', kwargs={'resource_id': self.res1.id}) + response = self.client.get(url, follow=True) + self.assertRedirects(response, reverse('account:login') + '?next=/booking/' + str( + self.res1.id) + '/') + + # authenticated user + 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/booking/urls.py b/tools/pharos-dashboard/booking/urls.py new file mode 100644 index 00000000..37f0c6b0 --- /dev/null +++ b/tools/pharos-dashboard/booking/urls.py @@ -0,0 +1,24 @@ +"""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'), +] diff --git a/tools/pharos-dashboard/booking/views.py b/tools/pharos-dashboard/booking/views.py new file mode 100644 index 00000000..bc00d3eb --- /dev/null +++ b/tools/pharos-dashboard/booking/views.py @@ -0,0 +1,50 @@ +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.http import JsonResponse +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.views import View +from django.views.generic import FormView + +from booking.forms import BookingForm +from booking.models import Booking +from dashboard.models import Resource + +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): + booking = Booking(start=form.cleaned_data['start'], end=form.cleaned_data['end'], + purpose=form.cleaned_data['purpose'], resource=self.resource, + user=self.request.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) + messages.add_message(self.request, messages.SUCCESS, 'Booking saved') + return super(BookingFormView, self).form_valid(form) + + +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') + return JsonResponse({'bookings': list(bookings)}) |