diff options
Diffstat (limited to 'tools/pharos-dashboard/src/booking')
-rw-r--r-- | tools/pharos-dashboard/src/booking/__init__.py | 10 | ||||
-rw-r--r-- | tools/pharos-dashboard/src/booking/admin.py | 17 | ||||
-rw-r--r-- | tools/pharos-dashboard/src/booking/apps.py | 15 | ||||
-rw-r--r-- | tools/pharos-dashboard/src/booking/forms.py | 23 | ||||
-rw-r--r-- | tools/pharos-dashboard/src/booking/migrations/0001_initial.py | 68 | ||||
-rw-r--r-- | tools/pharos-dashboard/src/booking/migrations/__init__.py | 10 | ||||
-rw-r--r-- | tools/pharos-dashboard/src/booking/models.py | 77 | ||||
-rw-r--r-- | tools/pharos-dashboard/src/booking/tests/__init__.py | 10 | ||||
-rw-r--r-- | tools/pharos-dashboard/src/booking/tests/test_models.py | 94 | ||||
-rw-r--r-- | tools/pharos-dashboard/src/booking/tests/test_views.py | 106 | ||||
-rw-r--r-- | tools/pharos-dashboard/src/booking/urls.py | 39 | ||||
-rw-r--r-- | tools/pharos-dashboard/src/booking/views.py | 122 |
12 files changed, 0 insertions, 591 deletions
diff --git a/tools/pharos-dashboard/src/booking/__init__.py b/tools/pharos-dashboard/src/booking/__init__.py deleted file mode 100644 index b5914ce7..00000000 --- a/tools/pharos-dashboard/src/booking/__init__.py +++ /dev/null @@ -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/tools/pharos-dashboard/src/booking/admin.py b/tools/pharos-dashboard/src/booking/admin.py deleted file mode 100644 index d883be1c..00000000 --- a/tools/pharos-dashboard/src/booking/admin.py +++ /dev/null @@ -1,17 +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 django.contrib import admin - -from booking.models import * - -admin.site.register(Booking) -admin.site.register(Installer) -admin.site.register(Scenario)
\ No newline at end of file diff --git a/tools/pharos-dashboard/src/booking/apps.py b/tools/pharos-dashboard/src/booking/apps.py deleted file mode 100644 index 99bf115f..00000000 --- a/tools/pharos-dashboard/src/booking/apps.py +++ /dev/null @@ -1,15 +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 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 deleted file mode 100644 index 2dbfacb0..00000000 --- a/tools/pharos-dashboard/src/booking/forms.py +++ /dev/null @@ -1,23 +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 - - -class BookingForm(forms.Form): - fields = ['start', 'end', 'purpose', 'installer', 'scenario'] - - start = forms.DateTimeField() - end = forms.DateTimeField() - purpose = forms.CharField(max_length=300) - installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=False) - scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=False)
\ No newline at end of file diff --git a/tools/pharos-dashboard/src/booking/migrations/0001_initial.py b/tools/pharos-dashboard/src/booking/migrations/0001_initial.py deleted file mode 100644 index 6932daea..00000000 --- a/tools/pharos-dashboard/src/booking/migrations/0001_initial.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2016-11-03 13:33 -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 = [ - ('dashboard', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Booking', - fields=[ - ('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)), - ('purpose', models.CharField(max_length=300)), - ], - options={ - 'db_table': 'booking', - }, - ), - migrations.CreateModel( - name='Installer', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=30)), - ], - ), - migrations.CreateModel( - name='Scenario', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=300)), - ], - ), - 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), - ), - ] diff --git a/tools/pharos-dashboard/src/booking/migrations/__init__.py b/tools/pharos-dashboard/src/booking/migrations/__init__.py deleted file mode 100644 index b5914ce7..00000000 --- a/tools/pharos-dashboard/src/booking/migrations/__init__.py +++ /dev/null @@ -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/tools/pharos-dashboard/src/booking/models.py b/tools/pharos-dashboard/src/booking/models.py deleted file mode 100644 index 0b3fa3b1..00000000 --- a/tools/pharos-dashboard/src/booking/models.py +++ /dev/null @@ -1,77 +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 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 dashboard.models import Resource - - -class Installer(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=30) - - def __str__(self): - return self.name - -class Scenario(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=300) - - def __str__(self): - return self.name - - -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) - - installer = models.ForeignKey(Installer, models.DO_NOTHING, null=True) - scenario = models.ForeignKey(Scenario, models.DO_NOTHING, null=True) - 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 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 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 deleted file mode 100644 index b5914ce7..00000000 --- a/tools/pharos-dashboard/src/booking/tests/__init__.py +++ /dev/null @@ -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/tools/pharos-dashboard/src/booking/tests/test_models.py b/tools/pharos-dashboard/src/booking/tests/test_models.py deleted file mode 100644 index b4cd1133..00000000 --- a/tools/pharos-dashboard/src/booking/tests/test_models.py +++ /dev/null @@ -1,94 +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.contrib.auth.models import Permission -from django.test import TestCase -from django.utils import timezone - -from booking.models import * -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) - - self.installer = Installer.objects.create(name='TestInstaller') - self.scenario = Scenario.objects.create(name='TestScenario') - - 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, scenario=self.scenario, - installer=self.installer))
\ No newline at end of file diff --git a/tools/pharos-dashboard/src/booking/tests/test_views.py b/tools/pharos-dashboard/src/booking/tests/test_views.py deleted file mode 100644 index c1da013c..00000000 --- a/tools/pharos-dashboard/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/tools/pharos-dashboard/src/booking/urls.py b/tools/pharos-dashboard/src/booking/urls.py deleted file mode 100644 index 9e013164..00000000 --- a/tools/pharos-dashboard/src/booking/urls.py +++ /dev/null @@ -1,39 +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 -############################################################################## - - -"""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'), - - url(r'^list/$', BookingListView.as_view(), name='list') -] diff --git a/tools/pharos-dashboard/src/booking/views.py b/tools/pharos-dashboard/src/booking/views.py deleted file mode 100644 index 6fdca0e0..00000000 --- a/tools/pharos-dashboard/src/booking/views.py +++ /dev/null @@ -1,122 +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 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.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(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): - 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(BookingFormView, self).form_invalid(form) - - user = self.request.user - booking = Booking(start=form.cleaned_data['start'], - end=form.cleaned_data['end'], - purpose=form.cleaned_data['purpose'], - installer=form.cleaned_data['installer'], - scenario=form.cleaned_data['scenario'], - 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) - 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']) - title = 'Booking Details' - context = super(BookingView, self).get_context_data(**kwargs) - context.update({'title': title, 'booking': booking}) - return context - - -class BookingListView(TemplateView): - template_name = "booking/booking_list.html" - - def get_context_data(self, **kwargs): - bookings = Booking.objects.filter(end__gte=timezone.now()) - title = 'Search Booking' - context = super(BookingListView, self).get_context_data(**kwargs) - context.update({'title': title, 'bookings': bookings}) - 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', - 'installer__name', 'scenario__name') - return JsonResponse({'bookings': list(bookings)}) |