summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/pharos-dashboard/.gitignore1
-rw-r--r--tools/pharos-dashboard/README.md1
-rw-r--r--tools/pharos-dashboard/TODO3
-rw-r--r--tools/pharos-dashboard/account/__init__.py0
-rw-r--r--tools/pharos-dashboard/account/admin.py5
-rw-r--r--tools/pharos-dashboard/account/apps.py5
-rw-r--r--tools/pharos-dashboard/account/forms.py12
-rw-r--r--tools/pharos-dashboard/account/jira_util.py56
-rw-r--r--tools/pharos-dashboard/account/middleware.py22
-rw-r--r--tools/pharos-dashboard/account/migrations/__init__.py0
-rw-r--r--tools/pharos-dashboard/account/models.py20
-rw-r--r--tools/pharos-dashboard/account/rsa.pem17
-rw-r--r--tools/pharos-dashboard/account/rsa.pub6
-rw-r--r--tools/pharos-dashboard/account/tests/__init__.py0
-rw-r--r--tools/pharos-dashboard/account/tests/test_general.py40
-rw-r--r--tools/pharos-dashboard/account/urls.py25
-rw-r--r--tools/pharos-dashboard/account/views.py111
-rw-r--r--tools/pharos-dashboard/booking/__init__.py0
-rw-r--r--tools/pharos-dashboard/booking/admin.py5
-rw-r--r--tools/pharos-dashboard/booking/apps.py5
-rw-r--r--tools/pharos-dashboard/booking/forms.py9
-rw-r--r--tools/pharos-dashboard/booking/migrations/__init__.py0
-rw-r--r--tools/pharos-dashboard/booking/models.py59
-rw-r--r--tools/pharos-dashboard/booking/tests/__init__.py0
-rw-r--r--tools/pharos-dashboard/booking/tests/test_models.py97
-rw-r--r--tools/pharos-dashboard/booking/tests/test_views.py71
-rw-r--r--tools/pharos-dashboard/booking/urls.py26
-rw-r--r--tools/pharos-dashboard/booking/views.py97
-rw-r--r--tools/pharos-dashboard/celerybeat-schedulebin0 -> 16384 bytes
-rw-r--r--tools/pharos-dashboard/dashboard/admin.py7
-rw-r--r--tools/pharos-dashboard/dashboard/fixtures/DBdata_resources.json1
-rw-r--r--tools/pharos-dashboard/dashboard/fixtures/DBdata_test_bookings.json1
-rw-r--r--tools/pharos-dashboard/dashboard/fixtures/DBdata_users.json1
-rw-r--r--tools/pharos-dashboard/dashboard/fixtures/dashboard.json164
-rw-r--r--tools/pharos-dashboard/dashboard/forms/booking_form.py37
-rw-r--r--tools/pharos-dashboard/dashboard/jenkins/jenkins_util.py70
-rw-r--r--tools/pharos-dashboard/dashboard/migrations/0001_initial.py107
-rw-r--r--tools/pharos-dashboard/dashboard/models.py82
-rw-r--r--tools/pharos-dashboard/dashboard/static/css/theme.css7
-rw-r--r--tools/pharos-dashboard/dashboard/static/js/booking-calendar.js68
-rw-r--r--tools/pharos-dashboard/dashboard/static/js/csrf.js34
-rw-r--r--tools/pharos-dashboard/dashboard/static/js/datetimepicker-options.js7
-rw-r--r--tools/pharos-dashboard/dashboard/static/js/fullcalendar-options.js65
-rw-r--r--tools/pharos-dashboard/dashboard/templatetags/__init__.py0
-rw-r--r--tools/pharos-dashboard/dashboard/templatetags/jenkins_filters.py28
-rw-r--r--tools/pharos-dashboard/dashboard/templatetags/jira_filters.py8
-rw-r--r--tools/pharos-dashboard/dashboard/urls.py47
-rw-r--r--tools/pharos-dashboard/dashboard/views.py78
-rw-r--r--tools/pharos-dashboard/dashboard/views/booking.py69
-rw-r--r--tools/pharos-dashboard/dashboard/views/registration.py16
-rw-r--r--tools/pharos-dashboard/dashboard/views/table_views.py62
-rw-r--r--tools/pharos-dashboard/issues.org6
-rw-r--r--tools/pharos-dashboard/jenkins/__init__.py0
-rw-r--r--tools/pharos-dashboard/jenkins/adapter.py (renamed from tools/pharos-dashboard/dashboard/jenkins/jenkins_adapter.py)46
-rw-r--r--tools/pharos-dashboard/jenkins/apps.py5
-rw-r--r--tools/pharos-dashboard/jenkins/migrations/__init__.py0
-rw-r--r--tools/pharos-dashboard/jenkins/models.py50
-rw-r--r--tools/pharos-dashboard/jenkins/tasks.py39
-rw-r--r--tools/pharos-dashboard/jenkins/tests.py (renamed from tools/pharos-dashboard/dashboard/tests.py)7
-rwxr-xr-xtools/pharos-dashboard/manage.py18
-rw-r--r--tools/pharos-dashboard/pharos_dashboard/__init__.py3
-rw-r--r--tools/pharos-dashboard/pharos_dashboard/celery.py20
-rw-r--r--tools/pharos-dashboard/pharos_dashboard/settings.py124
-rw-r--r--tools/pharos-dashboard/pharos_dashboard/urls.py12
-rw-r--r--tools/pharos-dashboard/pharos_dashboard/wsgi.py2
-rw-r--r--tools/pharos-dashboard/requirements.txt5
-rw-r--r--tools/pharos-dashboard/static/bower.json (renamed from tools/pharos-dashboard/dashboard/static/bower.json)0
-rw-r--r--tools/pharos-dashboard/static/css/theme.css13
-rw-r--r--tools/pharos-dashboard/static/js/booking-calendar.js36
-rw-r--r--tools/pharos-dashboard/static/js/dataTables-sort.js (renamed from tools/pharos-dashboard/dashboard/static/js/dataTables-sort.js)0
-rw-r--r--tools/pharos-dashboard/static/js/datetimepicker-options.js3
-rw-r--r--tools/pharos-dashboard/static/js/fullcalendar-options.js91
-rw-r--r--tools/pharos-dashboard/templates/account/userprofile_update_form.html30
-rw-r--r--tools/pharos-dashboard/templates/base.html (renamed from tools/pharos-dashboard/templates/dashboard/base.html)30
-rw-r--r--tools/pharos-dashboard/templates/booking/booking_calendar.html (renamed from tools/pharos-dashboard/templates/dashboard/booking_calendar.html)81
-rw-r--r--tools/pharos-dashboard/templates/booking/booking_detail.html26
-rw-r--r--tools/pharos-dashboard/templates/booking/booking_table.html33
-rw-r--r--tools/pharos-dashboard/templates/dashboard/ci_pods.html (renamed from tools/pharos-dashboard/templates/tables/ci_pods.html)27
-rw-r--r--tools/pharos-dashboard/templates/dashboard/dev_pods.html (renamed from tools/pharos-dashboard/templates/tables/dev_pods.html)19
-rw-r--r--tools/pharos-dashboard/templates/dashboard/jenkins_slaves.html (renamed from tools/pharos-dashboard/templates/tables/jenkins_slaves.html)13
-rw-r--r--tools/pharos-dashboard/templates/dashboard/resource.html58
-rw-r--r--tools/pharos-dashboard/templates/dashboard/resource_all.html74
-rw-r--r--tools/pharos-dashboard/templates/dashboard/resource_detail.html64
-rw-r--r--tools/pharos-dashboard/templates/dashboard/server_table.html30
-rw-r--r--tools/pharos-dashboard/templates/dashboard/table.html2
-rw-r--r--tools/pharos-dashboard/templates/layout.html (renamed from tools/pharos-dashboard/templates/layout/base.html)9
-rw-r--r--tools/pharos-dashboard/templates/registration/login.html61
87 files changed, 1753 insertions, 936 deletions
diff --git a/tools/pharos-dashboard/.gitignore b/tools/pharos-dashboard/.gitignore
index b5e9284a..9eb1cfde 100644
--- a/tools/pharos-dashboard/.gitignore
+++ b/tools/pharos-dashboard/.gitignore
@@ -19,6 +19,7 @@ coverage.xml
# Django:
*.log
*.pot
+migrations/
# KDE:
.directory
diff --git a/tools/pharos-dashboard/README.md b/tools/pharos-dashboard/README.md
deleted file mode 100644
index be27bf51..00000000
--- a/tools/pharos-dashboard/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# Pharos Dashboard
diff --git a/tools/pharos-dashboard/TODO b/tools/pharos-dashboard/TODO
deleted file mode 100644
index 1c539d6f..00000000
--- a/tools/pharos-dashboard/TODO
+++ /dev/null
@@ -1,3 +0,0 @@
-- implement ajax booking form to call from fullcalendar, then implement editing of multiple events
-- if the user is behind a VPN, his timezone settings might be wrong, there should be an option in the
-user settings to override the browser timezone.
diff --git a/tools/pharos-dashboard/account/__init__.py b/tools/pharos-dashboard/account/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tools/pharos-dashboard/account/__init__.py
diff --git a/tools/pharos-dashboard/account/admin.py b/tools/pharos-dashboard/account/admin.py
new file mode 100644
index 00000000..7fab1238
--- /dev/null
+++ b/tools/pharos-dashboard/account/admin.py
@@ -0,0 +1,5 @@
+from django.contrib import admin
+
+from account.models import UserProfile
+
+admin.site.register(UserProfile) \ No newline at end of file
diff --git a/tools/pharos-dashboard/account/apps.py b/tools/pharos-dashboard/account/apps.py
new file mode 100644
index 00000000..999566ca
--- /dev/null
+++ b/tools/pharos-dashboard/account/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class AccountsConfig(AppConfig):
+ name = 'account'
diff --git a/tools/pharos-dashboard/account/forms.py b/tools/pharos-dashboard/account/forms.py
new file mode 100644
index 00000000..92c55d85
--- /dev/null
+++ b/tools/pharos-dashboard/account/forms.py
@@ -0,0 +1,12 @@
+import django.forms as forms
+import pytz as pytz
+
+from account.models import UserProfile
+
+
+class AccountSettingsForm(forms.ModelForm):
+ class Meta:
+ model = UserProfile
+ fields = ['company', 'ssh_public_key', 'pgp_public_key', 'timezone']
+
+ timezone = forms.ChoiceField(choices=[(x, x) for x in pytz.common_timezones], initial='UTC')
diff --git a/tools/pharos-dashboard/account/jira_util.py b/tools/pharos-dashboard/account/jira_util.py
new file mode 100644
index 00000000..bd07ff3b
--- /dev/null
+++ b/tools/pharos-dashboard/account/jira_util.py
@@ -0,0 +1,56 @@
+import base64
+import os
+
+import oauth2 as oauth
+from jira import JIRA
+from tlslite.utils import keyfactory
+
+from pharos_dashboard import settings
+
+
+class SignatureMethod_RSA_SHA1(oauth.SignatureMethod):
+ name = 'RSA-SHA1'
+
+ def signing_base(self, request, consumer, token):
+ if not hasattr(request, 'normalized_url') or request.normalized_url is None:
+ raise ValueError("Base URL for request is not set.")
+
+ sig = (
+ oauth.escape(request.method),
+ oauth.escape(request.normalized_url),
+ oauth.escape(request.get_normalized_parameters()),
+ )
+
+ key = '%s&' % oauth.escape(consumer.secret)
+ if token:
+ key += oauth.escape(token.secret)
+ raw = '&'.join(sig)
+ return key, raw
+
+ def sign(self, request, consumer, token):
+ """Builds the base signature string."""
+ key, raw = self.signing_base(request, consumer, token)
+
+ module_dir = os.path.dirname(__file__) # get current directory
+ with open(module_dir + '/rsa.pem', 'r') as f:
+ data = f.read()
+ privateKeyString = data.strip()
+ privatekey = keyfactory.parsePrivateKey(privateKeyString)
+ raw = str.encode(raw)
+ signature = privatekey.hashAndSign(raw)
+ return base64.b64encode(signature)
+
+
+def get_jira(user):
+ module_dir = os.path.dirname(__file__) # get current directory
+ with open(module_dir + '/rsa.pem', 'r') as f:
+ key_cert = f.read()
+
+ oauth_dict = {
+ 'access_token': user.userprofile.oauth_token,
+ 'access_token_secret': user.userprofile.oauth_secret,
+ 'consumer_key': settings.OAUTH_CONSUMER_KEY,
+ 'key_cert': key_cert
+ }
+
+ return JIRA(server=settings.JIRA_URL, oauth=oauth_dict) \ No newline at end of file
diff --git a/tools/pharos-dashboard/account/middleware.py b/tools/pharos-dashboard/account/middleware.py
new file mode 100644
index 00000000..6f7cac7a
--- /dev/null
+++ b/tools/pharos-dashboard/account/middleware.py
@@ -0,0 +1,22 @@
+from django.utils import timezone
+from django.utils.deprecation import MiddlewareMixin
+
+from account.models import UserProfile
+
+
+class TimezoneMiddleware(MiddlewareMixin):
+ """
+ Activate the timezone from request.user.userprofile if user is authenticated,
+ deactivate the timezone otherwise and use default (UTC)
+ """
+ def process_request(self, request):
+ if request.user.is_authenticated:
+ try:
+ tz = request.user.userprofile.timezone
+ timezone.activate(tz)
+ except UserProfile.DoesNotExist:
+ UserProfile.objects.create(user=request.user)
+ tz = request.user.userprofile.timezone
+ timezone.activate(tz)
+ else:
+ timezone.deactivate()
diff --git a/tools/pharos-dashboard/account/migrations/__init__.py b/tools/pharos-dashboard/account/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tools/pharos-dashboard/account/migrations/__init__.py
diff --git a/tools/pharos-dashboard/account/models.py b/tools/pharos-dashboard/account/models.py
new file mode 100644
index 00000000..fb2c8ddd
--- /dev/null
+++ b/tools/pharos-dashboard/account/models.py
@@ -0,0 +1,20 @@
+from django.db import models
+
+from django.contrib.auth.models import User
+
+from dashboard.models import Resource
+
+def upload_to(object, filename):
+ return object.user.username + '/' + filename
+
+class UserProfile(models.Model):
+ user = models.OneToOneField(User, on_delete=models.CASCADE)
+ timezone = models.CharField(max_length=100, blank=False, default='UTC')
+ ssh_public_key = models.FileField(upload_to=upload_to, null=True, blank=True)
+ pgp_public_key = models.FileField(upload_to=upload_to, null=True, blank=True)
+ company = models.CharField(max_length=200, blank=False)
+ oauth_token = models.CharField(max_length=1024, blank=False)
+ oauth_secret = models.CharField(max_length=1024, blank=False)
+
+ class Meta:
+ db_table = 'user_profile'
diff --git a/tools/pharos-dashboard/account/rsa.pem b/tools/pharos-dashboard/account/rsa.pem
new file mode 100644
index 00000000..dbd4eedd
--- /dev/null
+++ b/tools/pharos-dashboard/account/rsa.pem
@@ -0,0 +1,17 @@
+-----BEGIN PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALRiMLAh9iimur8V
+A7qVvdqxevEuUkW4K+2KdMXmnQbG9Aa7k7eBjK1S+0LYmVjPKlJGNXHDGuy5Fw/d
+7rjVJ0BLB+ubPK8iA/Tw3hLQgXMRRGRXXCn8ikfuQfjUS1uZSatdLB81mydBETlJ
+hI6GH4twrbDJCR2Bwy/XWXgqgGRzAgMBAAECgYBYWVtleUzavkbrPjy0T5FMou8H
+X9u2AC2ry8vD/l7cqedtwMPp9k7TubgNFo+NGvKsl2ynyprOZR1xjQ7WgrgVB+mm
+uScOM/5HVceFuGRDhYTCObE+y1kxRloNYXnx3ei1zbeYLPCHdhxRYW7T0qcynNmw
+rn05/KO2RLjgQNalsQJBANeA3Q4Nugqy4QBUCEC09SqylT2K9FrrItqL2QKc9v0Z
+zO2uwllCbg0dwpVuYPYXYvikNHHg+aCWF+VXsb9rpPsCQQDWR9TT4ORdzoj+Nccn
+qkMsDmzt0EfNaAOwHOmVJ2RVBspPcxt5iN4HI7HNeG6U5YsFBb+/GZbgfBT3kpNG
+WPTpAkBI+gFhjfJvRw38n3g/+UeAkwMI2TJQS4n8+hid0uus3/zOjDySH3XHCUno
+cn1xOJAyZODBo47E+67R4jV1/gzbAkEAklJaspRPXP877NssM5nAZMU0/O/NGCZ+
+3jPgDUno6WbJn5cqm8MqWhW1xGkImgRk+fkDBquiq4gPiT898jusgQJAd5Zrr6Q8
+AO/0isr/3aa6O6NLQxISLKcPDk2NOccAfS/xOtfOz4sJYM3+Bs4Io9+dZGSDCA54
+Lw03eHTNQghS0A==
+-----END PRIVATE KEY-----
+
diff --git a/tools/pharos-dashboard/account/rsa.pub b/tools/pharos-dashboard/account/rsa.pub
new file mode 100644
index 00000000..cc50e45e
--- /dev/null
+++ b/tools/pharos-dashboard/account/rsa.pub
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0YjCwIfYoprq/FQO6lb3asXrx
+LlJFuCvtinTF5p0GxvQGu5O3gYytUvtC2JlYzypSRjVxwxrsuRcP3e641SdASwfr
+mzyvIgP08N4S0IFzEURkV1wp/IpH7kH41EtbmUmrXSwfNZsnQRE5SYSOhh+LcK2w
+yQkdgcMv11l4KoBkcwIDAQAB
+-----END PUBLIC KEY-----
diff --git a/tools/pharos-dashboard/account/tests/__init__.py b/tools/pharos-dashboard/account/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tools/pharos-dashboard/account/tests/__init__.py
diff --git a/tools/pharos-dashboard/account/tests/test_general.py b/tools/pharos-dashboard/account/tests/test_general.py
new file mode 100644
index 00000000..ba80b62c
--- /dev/null
+++ b/tools/pharos-dashboard/account/tests/test_general.py
@@ -0,0 +1,40 @@
+from django.contrib.auth.models import User
+from django.test import Client
+from django.test import TestCase
+from django.urls import reverse
+from django.utils import timezone
+
+from account.models import UserProfile
+
+
+class AccountMiddlewareTestCase(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.user1 = User.objects.create(username='user1')
+ self.user1.set_password('user1')
+ self.user1profile = UserProfile.objects.create(user=self.user1)
+ self.user1.save()
+
+ def test_timezone_middleware(self):
+ """
+ The timezone should be UTC for anonymous users, for authenticated users it should be set
+ to user.userprofile.timezone
+ """
+ #default
+ self.assertEqual(timezone.get_current_timezone_name(), 'UTC')
+
+ url = reverse('account:settings')
+ # anonymous request
+ self.client.get(url)
+ self.assertEqual(timezone.get_current_timezone_name(), 'UTC')
+
+ # authenticated user with UTC timezone (userprofile default)
+ self.client.login(username='user1', password='user1')
+ self.client.get(url)
+ self.assertEqual(timezone.get_current_timezone_name(), 'UTC')
+
+ # authenticated user with custom timezone (userprofile default)
+ self.user1profile.timezone = 'Etc/Greenwich'
+ self.user1profile.save()
+ self.client.get(url)
+ self.assertEqual(timezone.get_current_timezone_name(), 'Etc/Greenwich')
diff --git a/tools/pharos-dashboard/account/urls.py b/tools/pharos-dashboard/account/urls.py
new file mode 100644
index 00000000..b837814a
--- /dev/null
+++ b/tools/pharos-dashboard/account/urls.py
@@ -0,0 +1,25 @@
+"""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 account.views import *
+
+urlpatterns = [
+ url(r'^settings/', AccountSettingsView.as_view(), name='settings'),
+ url(r'^authenticated/$', JiraAuthenticatedView.as_view(), name='authenticated'),
+ url(r'^login/$', JiraLoginView.as_view(), name='login'),
+ url(r'^logout/$', JiraLogoutView.as_view(), name='logout')
+]
diff --git a/tools/pharos-dashboard/account/views.py b/tools/pharos-dashboard/account/views.py
new file mode 100644
index 00000000..7d2c9bd0
--- /dev/null
+++ b/tools/pharos-dashboard/account/views.py
@@ -0,0 +1,111 @@
+import os
+import urllib
+
+import oauth2 as oauth
+from django.contrib import messages
+from django.contrib.auth import logout, authenticate, login
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.contrib.auth.models import User
+from django.urls import reverse
+from django.utils.decorators import method_decorator
+from django.views.generic import RedirectView
+from django.views.generic import UpdateView
+from jira import JIRA
+
+from account.forms import AccountSettingsForm
+from account.jira_util import SignatureMethod_RSA_SHA1
+from account.models import UserProfile
+from pharos_dashboard import settings
+
+consumer = oauth.Consumer(settings.OAUTH_CONSUMER_KEY, settings.OAUTH_CONSUMER_SECRET)
+
+
+@method_decorator(login_required, name='dispatch')
+class AccountSettingsView(UpdateView):
+ model = UserProfile
+ form_class = AccountSettingsForm
+ template_name_suffix = '_update_form'
+
+ def get_success_url(self):
+ messages.add_message(self.request, messages.INFO,
+ 'Settings saved')
+ return '/'
+
+ def get_object(self, queryset=None):
+ return self.request.user.userprofile
+
+
+class JiraLoginView(RedirectView):
+ def get_redirect_url(self, *args, **kwargs):
+ client = oauth.Client(consumer)
+ client.set_signature_method(SignatureMethod_RSA_SHA1())
+
+ # Step 1. Get a request token from Jira.
+ resp, content = client.request(settings.OAUTH_REQUEST_TOKEN_URL, "POST")
+ if resp['status'] != '200':
+ raise Exception("Invalid response %s: %s" % (resp['status'], content))
+
+ # Step 2. Store the request token in a session for later use.
+ self.request.session['request_token'] = dict(urllib.parse.parse_qsl(content.decode()))
+ # Step 3. Redirect the user to the authentication URL.
+ url = settings.OAUTH_AUTHORIZE_URL + '?oauth_token=' + \
+ self.request.session['request_token']['oauth_token']
+ return url
+
+
+class JiraLogoutView(LoginRequiredMixin, RedirectView):
+ def get_redirect_url(self, *args, **kwargs):
+ logout(self.request)
+ return '/'
+
+
+class JiraAuthenticatedView(RedirectView):
+ def get_redirect_url(self, *args, **kwargs):
+ # Step 1. Use the request token in the session to build a new client.
+ token = oauth.Token(self.request.session['request_token']['oauth_token'],
+ self.request.session['request_token']['oauth_token_secret'])
+ client = oauth.Client(consumer, token)
+ client.set_signature_method(SignatureMethod_RSA_SHA1())
+
+ # Step 2. Request the authorized access token from Jira.
+ resp, content = client.request(settings.OAUTH_ACCESS_TOKEN_URL, "POST")
+ if resp['status'] != '200':
+ return '/'
+
+ access_token = dict(urllib.parse.parse_qsl(content.decode()))
+
+ module_dir = os.path.dirname(__file__) # get current directory
+ with open(module_dir + '/rsa.pem', 'r') as f:
+ key_cert = f.read()
+
+ oauth_dict = {
+ 'access_token': access_token['oauth_token'],
+ 'access_token_secret': access_token['oauth_token_secret'],
+ 'consumer_key': settings.OAUTH_CONSUMER_KEY,
+ 'key_cert': key_cert
+ }
+
+ jira = JIRA(server=settings.JIRA_URL, oauth=oauth_dict)
+ username = jira.current_user()
+ url = '/'
+ # Step 3. Lookup the user or create them if they don't exist.
+ try:
+ user = User.objects.get(username=username)
+ except User.DoesNotExist:
+ # Save our permanent token and secret for later.
+ user = User.objects.create_user(username=username,
+ password=access_token['oauth_token_secret'])
+ profile = UserProfile()
+ profile.user = user
+ profile.save()
+ url = reverse('account:settings')
+ user.userprofile.oauth_token = access_token['oauth_token']
+ user.userprofile.oauth_secret = access_token['oauth_token_secret']
+ user.userprofile.save()
+ user.set_password(access_token['oauth_token_secret'])
+ user.save()
+ user = authenticate(username=username, password=access_token['oauth_token_secret'])
+ login(self.request, user)
+ # redirect user to settings page to complete profile
+ return url
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/__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..4be8ccab
--- /dev/null
+++ b/tools/pharos-dashboard/booking/models.py
@@ -0,0 +1,59 @@
+from django.contrib.auth.models import User
+from django.db import models
+from jira import JIRA
+
+from dashboard.models import Resource
+from pharos_dashboard 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)
+
+ purpose = models.CharField(max_length=300, blank=False)
+
+ class Meta:
+ db_table = 'booking'
+
+ def get_jira_issue(self):
+ 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
+
+ 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/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..7a572c5f
--- /dev/null
+++ b/tools/pharos-dashboard/booking/tests/test_models.py
@@ -0,0 +1,97 @@
+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/booking/tests/test_views.py b/tools/pharos-dashboard/booking/tests/test_views.py
new file mode 100644
index 00000000..c5dff586
--- /dev/null
+++ b/tools/pharos-dashboard/booking/tests/test_views.py
@@ -0,0 +1,71 @@
+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/booking/urls.py b/tools/pharos-dashboard/booking/urls.py
new file mode 100644
index 00000000..f6429daa
--- /dev/null
+++ b/tools/pharos-dashboard/booking/urls.py
@@ -0,0 +1,26 @@
+"""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/booking/views.py b/tools/pharos-dashboard/booking/views.py
new file mode 100644
index 00000000..fde8d816
--- /dev/null
+++ b/tools/pharos-dashboard/booking/views.py
@@ -0,0 +1,97 @@
+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.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:
+ 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')
+ return JsonResponse({'bookings': list(bookings)})
diff --git a/tools/pharos-dashboard/celerybeat-schedule b/tools/pharos-dashboard/celerybeat-schedule
new file mode 100644
index 00000000..7e5fe853
--- /dev/null
+++ b/tools/pharos-dashboard/celerybeat-schedule
Binary files differ
diff --git a/tools/pharos-dashboard/dashboard/admin.py b/tools/pharos-dashboard/dashboard/admin.py
index 532173f0..71a1e7d2 100644
--- a/tools/pharos-dashboard/dashboard/admin.py
+++ b/tools/pharos-dashboard/dashboard/admin.py
@@ -1,9 +1,6 @@
from django.contrib import admin
-from .models import *
-# Register your models here.
-admin.site.register(Booking)
-admin.site.register(Pod)
+from dashboard.models import *
+
admin.site.register(Resource)
admin.site.register(Server)
-admin.site.register(UserResource) \ No newline at end of file
diff --git a/tools/pharos-dashboard/dashboard/fixtures/DBdata_resources.json b/tools/pharos-dashboard/dashboard/fixtures/DBdata_resources.json
deleted file mode 100644
index 5aaccf7a..00000000
--- a/tools/pharos-dashboard/dashboard/fixtures/DBdata_resources.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"model": "dashboard.pod", "pk": 91, "fields": {"resource": 95, "chassis": null}}, {"model": "dashboard.pod", "pk": 92, "fields": {"resource": 96, "chassis": null}}, {"model": "dashboard.pod", "pk": 93, "fields": {"resource": 97, "chassis": null}}, {"model": "dashboard.pod", "pk": 94, "fields": {"resource": 98, "chassis": null}}, {"model": "dashboard.pod", "pk": 95, "fields": {"resource": 99, "chassis": null}}, {"model": "dashboard.pod", "pk": 96, "fields": {"resource": 100, "chassis": null}}, {"model": "dashboard.pod", "pk": 97, "fields": {"resource": 101, "chassis": null}}, {"model": "dashboard.pod", "pk": 98, "fields": {"resource": 102, "chassis": null}}, {"model": "dashboard.pod", "pk": 99, "fields": {"resource": 103, "chassis": null}}, {"model": "dashboard.pod", "pk": 100, "fields": {"resource": 104, "chassis": null}}, {"model": "dashboard.pod", "pk": 101, "fields": {"resource": 105, "chassis": null}}, {"model": "dashboard.pod", "pk": 102, "fields": {"resource": 106, "chassis": null}}, {"model": "dashboard.pod", "pk": 103, "fields": {"resource": 107, "chassis": null}}, {"model": "dashboard.pod", "pk": 104, "fields": {"resource": 108, "chassis": null}}, {"model": "dashboard.pod", "pk": 105, "fields": {"resource": 109, "chassis": null}}, {"model": "dashboard.pod", "pk": 106, "fields": {"resource": 110, "chassis": null}}, {"model": "dashboard.pod", "pk": 107, "fields": {"resource": 111, "chassis": null}}, {"model": "dashboard.pod", "pk": 108, "fields": {"resource": 112, "chassis": null}}, {"model": "dashboard.resource", "pk": 95, "fields": {"name": "Linux Foundation POD 1", "slavename": "lf-pod1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Lf+Lab", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 96, "fields": {"name": "Linux Foundation POD 2", "slavename": "lf-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Lf+Lab", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 97, "fields": {"name": "Ericsson POD 2", "slavename": "ericsson-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Ericsson+Hosting+and+Request+Process", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 98, "fields": {"name": "Intel POD 2", "slavename": "intel-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod2", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 99, "fields": {"name": "Intel POD 5", "slavename": "intel-pod5", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod5", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 100, "fields": {"name": "Intel POD 6", "slavename": "intel-pod6", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod6", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 101, "fields": {"name": "Intel POD 8", "slavename": "intel-pod8", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod8", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 102, "fields": {"name": "Huawei POD 1", "slavename": "huawei-pod1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 103, "fields": {"name": "Intel POD 3", "slavename": "intel-pod3", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod3", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 104, "fields": {"name": "Dell POD 1", "slavename": "dell-pod1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Dell+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 105, "fields": {"name": "Dell POD 2", "slavename": "dell-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Dell+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 106, "fields": {"name": "Orange POD 2", "slavename": "orange-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Opnfv-orange-pod2", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 107, "fields": {"name": "Arm POD 1", "slavename": "arm-build1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Enea-pharos-lab", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 108, "fields": {"name": "Ericsson POD 1", "slavename": "ericsson-pod1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Ericsson+Hosting+and+Request+Process", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 109, "fields": {"name": "Huawei POD 2", "slavename": "huawei-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 110, "fields": {"name": "Huawei POD 3", "slavename": "huawei-pod3", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 111, "fields": {"name": "Huawei POD 4", "slavename": "huawei-pod4", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 112, "fields": {"name": "Intel POD 9", "slavename": "intel-pod8", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod9", "bookable": false, "active": true}}]
diff --git a/tools/pharos-dashboard/dashboard/fixtures/DBdata_test_bookings.json b/tools/pharos-dashboard/dashboard/fixtures/DBdata_test_bookings.json
deleted file mode 100644
index 19132810..00000000
--- a/tools/pharos-dashboard/dashboard/fixtures/DBdata_test_bookings.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"model": "dashboard.pod", "pk": 91, "fields": {"resource": 95, "chassis": null}}, {"model": "dashboard.pod", "pk": 92, "fields": {"resource": 96, "chassis": null}}, {"model": "dashboard.pod", "pk": 93, "fields": {"resource": 97, "chassis": null}}, {"model": "dashboard.pod", "pk": 94, "fields": {"resource": 98, "chassis": null}}, {"model": "dashboard.pod", "pk": 95, "fields": {"resource": 99, "chassis": null}}, {"model": "dashboard.pod", "pk": 96, "fields": {"resource": 100, "chassis": null}}, {"model": "dashboard.pod", "pk": 97, "fields": {"resource": 101, "chassis": null}}, {"model": "dashboard.pod", "pk": 98, "fields": {"resource": 102, "chassis": null}}, {"model": "dashboard.pod", "pk": 99, "fields": {"resource": 103, "chassis": null}}, {"model": "dashboard.pod", "pk": 100, "fields": {"resource": 104, "chassis": null}}, {"model": "dashboard.pod", "pk": 101, "fields": {"resource": 105, "chassis": null}}, {"model": "dashboard.pod", "pk": 102, "fields": {"resource": 106, "chassis": null}}, {"model": "dashboard.pod", "pk": 103, "fields": {"resource": 107, "chassis": null}}, {"model": "dashboard.pod", "pk": 104, "fields": {"resource": 108, "chassis": null}}, {"model": "dashboard.pod", "pk": 105, "fields": {"resource": 109, "chassis": null}}, {"model": "dashboard.pod", "pk": 106, "fields": {"resource": 110, "chassis": null}}, {"model": "dashboard.pod", "pk": 107, "fields": {"resource": 111, "chassis": null}}, {"model": "dashboard.pod", "pk": 108, "fields": {"resource": 112, "chassis": null}}, {"model": "dashboard.resource", "pk": 95, "fields": {"name": "Linux Foundation POD 1", "slavename": "lf-pod1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Lf+Lab", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 96, "fields": {"name": "Linux Foundation POD 2", "slavename": "lf-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Lf+Lab", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 97, "fields": {"name": "Ericsson POD 2", "slavename": "ericsson-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Ericsson+Hosting+and+Request+Process", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 98, "fields": {"name": "Intel POD 2", "slavename": "intel-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod2", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 99, "fields": {"name": "Intel POD 5", "slavename": "intel-pod5", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod5", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 100, "fields": {"name": "Intel POD 6", "slavename": "intel-pod6", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod6", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 101, "fields": {"name": "Intel POD 8", "slavename": "intel-pod8", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod8", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 102, "fields": {"name": "Huawei POD 1", "slavename": "huawei-pod1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 103, "fields": {"name": "Intel POD 3", "slavename": "intel-pod3", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod3", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 104, "fields": {"name": "Dell POD 1", "slavename": "dell-pod1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Dell+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 105, "fields": {"name": "Dell POD 2", "slavename": "dell-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Dell+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 106, "fields": {"name": "Orange POD 2", "slavename": "orange-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Opnfv-orange-pod2", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 107, "fields": {"name": "Arm POD 1", "slavename": "arm-build1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Enea-pharos-lab", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 108, "fields": {"name": "Ericsson POD 1", "slavename": "ericsson-pod1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Ericsson+Hosting+and+Request+Process", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 109, "fields": {"name": "Huawei POD 2", "slavename": "huawei-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 110, "fields": {"name": "Huawei POD 3", "slavename": "huawei-pod3", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 111, "fields": {"name": "Huawei POD 4", "slavename": "huawei-pod4", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 112, "fields": {"name": "Intel POD 9", "slavename": "intel-pod8", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod9", "bookable": false, "active": true}}, {"model": "contenttypes.contenttype", "pk": 1, "fields": {"app_label": "dashboard", "model": "booking"}}, {"model": "contenttypes.contenttype", "pk": 2, "fields": {"app_label": "dashboard", "model": "pod"}}, {"model": "contenttypes.contenttype", "pk": 3, "fields": {"app_label": "dashboard", "model": "resource"}}, {"model": "contenttypes.contenttype", "pk": 4, "fields": {"app_label": "dashboard", "model": "server"}}, {"model": "contenttypes.contenttype", "pk": 5, "fields": {"app_label": "dashboard", "model": "userresource"}}, {"model": "contenttypes.contenttype", "pk": 6, "fields": {"app_label": "admin", "model": "logentry"}}, {"model": "contenttypes.contenttype", "pk": 7, "fields": {"app_label": "auth", "model": "permission"}}, {"model": "contenttypes.contenttype", "pk": 8, "fields": {"app_label": "auth", "model": "group"}}, {"model": "contenttypes.contenttype", "pk": 9, "fields": {"app_label": "auth", "model": "user"}}, {"model": "contenttypes.contenttype", "pk": 10, "fields": {"app_label": "contenttypes", "model": "contenttype"}}, {"model": "contenttypes.contenttype", "pk": 11, "fields": {"app_label": "sessions", "model": "session"}}, {"model": "auth.permission", "pk": 1, "fields": {"name": "Can add booking", "content_type": 1, "codename": "add_booking"}}, {"model": "auth.permission", "pk": 2, "fields": {"name": "Can change booking", "content_type": 1, "codename": "change_booking"}}, {"model": "auth.permission", "pk": 3, "fields": {"name": "Can delete booking", "content_type": 1, "codename": "delete_booking"}}, {"model": "auth.permission", "pk": 4, "fields": {"name": "Can add pod", "content_type": 2, "codename": "add_pod"}}, {"model": "auth.permission", "pk": 5, "fields": {"name": "Can change pod", "content_type": 2, "codename": "change_pod"}}, {"model": "auth.permission", "pk": 6, "fields": {"name": "Can delete pod", "content_type": 2, "codename": "delete_pod"}}, {"model": "auth.permission", "pk": 7, "fields": {"name": "Can add resource", "content_type": 3, "codename": "add_resource"}}, {"model": "auth.permission", "pk": 8, "fields": {"name": "Can change resource", "content_type": 3, "codename": "change_resource"}}, {"model": "auth.permission", "pk": 9, "fields": {"name": "Can delete resource", "content_type": 3, "codename": "delete_resource"}}, {"model": "auth.permission", "pk": 10, "fields": {"name": "Can add server", "content_type": 4, "codename": "add_server"}}, {"model": "auth.permission", "pk": 11, "fields": {"name": "Can change server", "content_type": 4, "codename": "change_server"}}, {"model": "auth.permission", "pk": 12, "fields": {"name": "Can delete server", "content_type": 4, "codename": "delete_server"}}, {"model": "auth.permission", "pk": 13, "fields": {"name": "Can add user resource", "content_type": 5, "codename": "add_userresource"}}, {"model": "auth.permission", "pk": 14, "fields": {"name": "Can change user resource", "content_type": 5, "codename": "change_userresource"}}, {"model": "auth.permission", "pk": 15, "fields": {"name": "Can delete user resource", "content_type": 5, "codename": "delete_userresource"}}, {"model": "auth.permission", "pk": 16, "fields": {"name": "Can add log entry", "content_type": 6, "codename": "add_logentry"}}, {"model": "auth.permission", "pk": 17, "fields": {"name": "Can change log entry", "content_type": 6, "codename": "change_logentry"}}, {"model": "auth.permission", "pk": 18, "fields": {"name": "Can delete log entry", "content_type": 6, "codename": "delete_logentry"}}, {"model": "auth.permission", "pk": 19, "fields": {"name": "Can add permission", "content_type": 7, "codename": "add_permission"}}, {"model": "auth.permission", "pk": 20, "fields": {"name": "Can change permission", "content_type": 7, "codename": "change_permission"}}, {"model": "auth.permission", "pk": 21, "fields": {"name": "Can delete permission", "content_type": 7, "codename": "delete_permission"}}, {"model": "auth.permission", "pk": 22, "fields": {"name": "Can add group", "content_type": 8, "codename": "add_group"}}, {"model": "auth.permission", "pk": 23, "fields": {"name": "Can change group", "content_type": 8, "codename": "change_group"}}, {"model": "auth.permission", "pk": 24, "fields": {"name": "Can delete group", "content_type": 8, "codename": "delete_group"}}, {"model": "auth.permission", "pk": 25, "fields": {"name": "Can add user", "content_type": 9, "codename": "add_user"}}, {"model": "auth.permission", "pk": 26, "fields": {"name": "Can change user", "content_type": 9, "codename": "change_user"}}, {"model": "auth.permission", "pk": 27, "fields": {"name": "Can delete user", "content_type": 9, "codename": "delete_user"}}, {"model": "auth.permission", "pk": 28, "fields": {"name": "Can add content type", "content_type": 10, "codename": "add_contenttype"}}, {"model": "auth.permission", "pk": 29, "fields": {"name": "Can change content type", "content_type": 10, "codename": "change_contenttype"}}, {"model": "auth.permission", "pk": 30, "fields": {"name": "Can delete content type", "content_type": 10, "codename": "delete_contenttype"}}, {"model": "auth.permission", "pk": 31, "fields": {"name": "Can add session", "content_type": 11, "codename": "add_session"}}, {"model": "auth.permission", "pk": 32, "fields": {"name": "Can change session", "content_type": 11, "codename": "change_session"}}, {"model": "auth.permission", "pk": 33, "fields": {"name": "Can delete session", "content_type": 11, "codename": "delete_session"}}, {"model": "auth.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$24000$kcQ5X8WQg1tH$KHynJPYTsoBq7vAipLsP0EZyo3qAu1VGJwaoEQeUz3E=", "last_login": "2016-07-24T13:01:31.199Z", "is_superuser": true, "username": "max", "first_name": "", "last_name": "", "email": "max.breitenfeldt@gmail.com", "is_staff": true, "is_active": true, "date_joined": "2016-07-24T12:49:54.488Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 2, "fields": {"password": "pbkdf2_sha256$24000$6z3xKCpZp0xl$76IaKRzJocbGmGG9JUtPz3is2AjlMEIfb9omiTEn2N8=", "last_login": null, "is_superuser": true, "username": "jose", "first_name": "Jose", "last_name": "Lausuch", "email": "jose.lausuch@ericsson.com", "is_staff": true, "is_active": true, "date_joined": "2016-07-24T12:51:30Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 3, "fields": {"password": "pbkdf2_sha256$24000$466rSOMbdzZk$3TXRz9CRoephCrjNbEa7+nLJ4xwz4W0jvTfs4A69D1o=", "last_login": null, "is_superuser": true, "username": "daniel", "first_name": "Daniel", "last_name": "Smith", "email": "daniel.smith@ericsson.com", "is_staff": true, "is_active": true, "date_joined": "2016-07-24T12:52:23Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 4, "fields": {"password": "pbkdf2_sha256$24000$wjeFIjdiblRl$6ugfj1neDEsaxMQbQ0xV+zg/FKw8ImIDyuPqYWkUxE4=", "last_login": null, "is_superuser": true, "username": "jack", "first_name": "Jack", "last_name": "Morgan", "email": "jack.morgan@intel.com", "is_staff": true, "is_active": true, "date_joined": "2016-07-24T12:53:02Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 5, "fields": {"password": "pbkdf2_sha256$24000$qYMcbKTahNGK$fdPgcQKTKhjSHDZx+C2bymU46HpIl/0n5vbXMpplXWE=", "last_login": null, "is_superuser": true, "username": "fatih", "first_name": "Fatih", "last_name": "Degirmenci", "email": "fatih.degirmenci@ericsson.com", "is_staff": true, "is_active": true, "date_joined": "2016-07-24T12:53:38Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 6, "fields": {"password": "pbkdf2_sha256$24000$Ndxyrota7u3U$KWmjRYo3GW25pcgf9NUuz5PSXvH8hU8E2dUARoKY7EI=", "last_login": null, "is_superuser": true, "username": "trevor", "first_name": "Trevor", "last_name": "Cooper", "email": "trevor.cooper@intel.com", "is_staff": true, "is_active": true, "date_joined": "2016-07-24T12:54:32Z", "groups": [], "user_permissions": []}}, {"model": "admin.logentry", "pk": 1, "fields": {"action_time": "2016-07-24T12:51:30.147Z", "user": 1, "content_type": 9, "object_id": "2", "object_repr": "jose", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 2, "fields": {"action_time": "2016-07-24T12:51:58.928Z", "user": 1, "content_type": 9, "object_id": "2", "object_repr": "jose", "action_flag": 2, "change_message": "Changed first_name, last_name, email, is_staff and is_superuser."}}, {"model": "admin.logentry", "pk": 3, "fields": {"action_time": "2016-07-24T12:52:23.821Z", "user": 1, "content_type": 9, "object_id": "3", "object_repr": "daniel", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 4, "fields": {"action_time": "2016-07-24T12:52:40.446Z", "user": 1, "content_type": 9, "object_id": "3", "object_repr": "daniel", "action_flag": 2, "change_message": "Changed first_name, last_name, email, is_staff and is_superuser."}}, {"model": "admin.logentry", "pk": 5, "fields": {"action_time": "2016-07-24T12:52:45.866Z", "user": 1, "content_type": 9, "object_id": "3", "object_repr": "daniel", "action_flag": 2, "change_message": "No fields changed."}}, {"model": "admin.logentry", "pk": 6, "fields": {"action_time": "2016-07-24T12:53:02.303Z", "user": 1, "content_type": 9, "object_id": "4", "object_repr": "jack", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 7, "fields": {"action_time": "2016-07-24T12:53:22.399Z", "user": 1, "content_type": 9, "object_id": "4", "object_repr": "jack", "action_flag": 2, "change_message": "Changed first_name, last_name, email, is_staff and is_superuser."}}, {"model": "admin.logentry", "pk": 8, "fields": {"action_time": "2016-07-24T12:53:24.541Z", "user": 1, "content_type": 9, "object_id": "4", "object_repr": "jack", "action_flag": 2, "change_message": "No fields changed."}}, {"model": "admin.logentry", "pk": 9, "fields": {"action_time": "2016-07-24T12:53:38.910Z", "user": 1, "content_type": 9, "object_id": "5", "object_repr": "fatih", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 10, "fields": {"action_time": "2016-07-24T12:53:54.585Z", "user": 1, "content_type": 9, "object_id": "5", "object_repr": "fatih", "action_flag": 2, "change_message": "Changed first_name, last_name, email, is_staff and is_superuser."}}, {"model": "admin.logentry", "pk": 11, "fields": {"action_time": "2016-07-24T12:53:56.777Z", "user": 1, "content_type": 9, "object_id": "5", "object_repr": "fatih", "action_flag": 2, "change_message": "No fields changed."}}, {"model": "admin.logentry", "pk": 12, "fields": {"action_time": "2016-07-24T12:54:32.683Z", "user": 1, "content_type": 9, "object_id": "6", "object_repr": "trevor", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 13, "fields": {"action_time": "2016-07-24T12:54:53.188Z", "user": 1, "content_type": 9, "object_id": "6", "object_repr": "trevor", "action_flag": 2, "change_message": "Changed first_name, last_name, email, is_staff and is_superuser."}}, {"model": "admin.logentry", "pk": 14, "fields": {"action_time": "2016-07-24T12:55:45.789Z", "user": 1, "content_type": 5, "object_id": "1", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 15, "fields": {"action_time": "2016-07-24T12:55:51.347Z", "user": 1, "content_type": 5, "object_id": "2", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 16, "fields": {"action_time": "2016-07-24T12:55:56.704Z", "user": 1, "content_type": 5, "object_id": "3", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 17, "fields": {"action_time": "2016-07-24T12:56:08.238Z", "user": 1, "content_type": 5, "object_id": "4", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 18, "fields": {"action_time": "2016-07-24T12:56:17.849Z", "user": 1, "content_type": 5, "object_id": "5", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 19, "fields": {"action_time": "2016-07-24T12:56:24.215Z", "user": 1, "content_type": 5, "object_id": "6", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 20, "fields": {"action_time": "2016-07-24T12:56:33.608Z", "user": 1, "content_type": 5, "object_id": "7", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 21, "fields": {"action_time": "2016-07-24T12:56:37.554Z", "user": 1, "content_type": 5, "object_id": "8", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "dashboard.userresource", "pk": 1, "fields": {"user": 3, "resource": 108}}, {"model": "dashboard.userresource", "pk": 2, "fields": {"user": 3, "resource": 97}}, {"model": "dashboard.userresource", "pk": 3, "fields": {"user": 4, "resource": 98}}, {"model": "dashboard.userresource", "pk": 4, "fields": {"user": 4, "resource": 103}}, {"model": "dashboard.userresource", "pk": 5, "fields": {"user": 4, "resource": 99}}, {"model": "dashboard.userresource", "pk": 6, "fields": {"user": 4, "resource": 100}}, {"model": "dashboard.userresource", "pk": 7, "fields": {"user": 4, "resource": 101}}, {"model": "dashboard.userresource", "pk": 8, "fields": {"user": 4, "resource": 112}}, {"model": "dashboard.booking", "pk": 1, "fields": {"resource": 103, "user": 1, "start_date_time": "2016-07-23T00:00:00Z", "end_date_time": "2016-08-08T00:00:00Z", "creation": "2016-07-24T13:01:47.945Z", "purpose": "Test1"}}, {"model": "dashboard.booking", "pk": 2, "fields": {"resource": 103, "user": 1, "start_date_time": "2016-07-18T03:00:00Z", "end_date_time": "2016-07-20T10:00:00Z", "creation": "2016-07-24T13:02:00.033Z", "purpose": "test2"}}, {"model": "dashboard.booking", "pk": 3, "fields": {"resource": 109, "user": 1, "start_date_time": "2016-07-23T06:00:00Z", "end_date_time": "2016-07-26T17:00:00Z", "creation": "2016-07-24T13:02:23.024Z", "purpose": "test3"}}, {"model": "dashboard.booking", "pk": 4, "fields": {"resource": 110, "user": 1, "start_date_time": "2016-07-09T00:00:00Z", "end_date_time": "2016-08-01T00:00:00Z", "creation": "2016-07-24T13:02:35.138Z", "purpose": "test4"}}, {"model": "dashboard.booking", "pk": 5, "fields": {"resource": 111, "user": 1, "start_date_time": "2016-07-20T07:00:00Z", "end_date_time": "2016-07-20T10:00:00Z", "creation": "2016-07-24T13:02:45.153Z", "purpose": "test5"}}, {"model": "dashboard.booking", "pk": 6, "fields": {"resource": 111, "user": 1, "start_date_time": "2016-07-22T03:00:00Z", "end_date_time": "2016-07-24T10:00:00Z", "creation": "2016-07-24T13:02:50.050Z", "purpose": "test6"}}, {"model": "dashboard.booking", "pk": 7, "fields": {"resource": 111, "user": 1, "start_date_time": "2016-07-24T11:00:00Z", "end_date_time": "2016-07-24T19:00:00Z", "creation": "2016-07-24T13:02:57.207Z", "purpose": "test7"}}] \ No newline at end of file
diff --git a/tools/pharos-dashboard/dashboard/fixtures/DBdata_users.json b/tools/pharos-dashboard/dashboard/fixtures/DBdata_users.json
deleted file mode 100644
index d21be1fa..00000000
--- a/tools/pharos-dashboard/dashboard/fixtures/DBdata_users.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"model": "dashboard.pod", "pk": 91, "fields": {"resource": 95, "chassis": null}}, {"model": "dashboard.pod", "pk": 92, "fields": {"resource": 96, "chassis": null}}, {"model": "dashboard.pod", "pk": 93, "fields": {"resource": 97, "chassis": null}}, {"model": "dashboard.pod", "pk": 94, "fields": {"resource": 98, "chassis": null}}, {"model": "dashboard.pod", "pk": 95, "fields": {"resource": 99, "chassis": null}}, {"model": "dashboard.pod", "pk": 96, "fields": {"resource": 100, "chassis": null}}, {"model": "dashboard.pod", "pk": 97, "fields": {"resource": 101, "chassis": null}}, {"model": "dashboard.pod", "pk": 98, "fields": {"resource": 102, "chassis": null}}, {"model": "dashboard.pod", "pk": 99, "fields": {"resource": 103, "chassis": null}}, {"model": "dashboard.pod", "pk": 100, "fields": {"resource": 104, "chassis": null}}, {"model": "dashboard.pod", "pk": 101, "fields": {"resource": 105, "chassis": null}}, {"model": "dashboard.pod", "pk": 102, "fields": {"resource": 106, "chassis": null}}, {"model": "dashboard.pod", "pk": 103, "fields": {"resource": 107, "chassis": null}}, {"model": "dashboard.pod", "pk": 104, "fields": {"resource": 108, "chassis": null}}, {"model": "dashboard.pod", "pk": 105, "fields": {"resource": 109, "chassis": null}}, {"model": "dashboard.pod", "pk": 106, "fields": {"resource": 110, "chassis": null}}, {"model": "dashboard.pod", "pk": 107, "fields": {"resource": 111, "chassis": null}}, {"model": "dashboard.pod", "pk": 108, "fields": {"resource": 112, "chassis": null}}, {"model": "dashboard.resource", "pk": 95, "fields": {"name": "Linux Foundation POD 1", "slavename": "lf-pod1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Lf+Lab", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 96, "fields": {"name": "Linux Foundation POD 2", "slavename": "lf-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Lf+Lab", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 97, "fields": {"name": "Ericsson POD 2", "slavename": "ericsson-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Ericsson+Hosting+and+Request+Process", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 98, "fields": {"name": "Intel POD 2", "slavename": "intel-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod2", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 99, "fields": {"name": "Intel POD 5", "slavename": "intel-pod5", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod5", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 100, "fields": {"name": "Intel POD 6", "slavename": "intel-pod6", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod6", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 101, "fields": {"name": "Intel POD 8", "slavename": "intel-pod8", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod8", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 102, "fields": {"name": "Huawei POD 1", "slavename": "huawei-pod1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 103, "fields": {"name": "Intel POD 3", "slavename": "intel-pod3", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod3", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 104, "fields": {"name": "Dell POD 1", "slavename": "dell-pod1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Dell+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 105, "fields": {"name": "Dell POD 2", "slavename": "dell-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Dell+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 106, "fields": {"name": "Orange POD 2", "slavename": "orange-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Opnfv-orange-pod2", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 107, "fields": {"name": "Arm POD 1", "slavename": "arm-build1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Enea-pharos-lab", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 108, "fields": {"name": "Ericsson POD 1", "slavename": "ericsson-pod1", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Ericsson+Hosting+and+Request+Process", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 109, "fields": {"name": "Huawei POD 2", "slavename": "huawei-pod2", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 110, "fields": {"name": "Huawei POD 3", "slavename": "huawei-pod3", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 111, "fields": {"name": "Huawei POD 4", "slavename": "huawei-pod4", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting", "bookable": false, "active": true}}, {"model": "dashboard.resource", "pk": 112, "fields": {"name": "Intel POD 9", "slavename": "intel-pod8", "description": "Some description", "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod9", "bookable": false, "active": true}}, {"model": "contenttypes.contenttype", "pk": 1, "fields": {"app_label": "dashboard", "model": "booking"}}, {"model": "contenttypes.contenttype", "pk": 2, "fields": {"app_label": "dashboard", "model": "pod"}}, {"model": "contenttypes.contenttype", "pk": 3, "fields": {"app_label": "dashboard", "model": "resource"}}, {"model": "contenttypes.contenttype", "pk": 4, "fields": {"app_label": "dashboard", "model": "server"}}, {"model": "contenttypes.contenttype", "pk": 5, "fields": {"app_label": "dashboard", "model": "userresource"}}, {"model": "contenttypes.contenttype", "pk": 6, "fields": {"app_label": "admin", "model": "logentry"}}, {"model": "contenttypes.contenttype", "pk": 7, "fields": {"app_label": "auth", "model": "permission"}}, {"model": "contenttypes.contenttype", "pk": 8, "fields": {"app_label": "auth", "model": "group"}}, {"model": "contenttypes.contenttype", "pk": 9, "fields": {"app_label": "auth", "model": "user"}}, {"model": "contenttypes.contenttype", "pk": 10, "fields": {"app_label": "contenttypes", "model": "contenttype"}}, {"model": "contenttypes.contenttype", "pk": 11, "fields": {"app_label": "sessions", "model": "session"}}, {"model": "auth.permission", "pk": 1, "fields": {"name": "Can add booking", "content_type": 1, "codename": "add_booking"}}, {"model": "auth.permission", "pk": 2, "fields": {"name": "Can change booking", "content_type": 1, "codename": "change_booking"}}, {"model": "auth.permission", "pk": 3, "fields": {"name": "Can delete booking", "content_type": 1, "codename": "delete_booking"}}, {"model": "auth.permission", "pk": 4, "fields": {"name": "Can add pod", "content_type": 2, "codename": "add_pod"}}, {"model": "auth.permission", "pk": 5, "fields": {"name": "Can change pod", "content_type": 2, "codename": "change_pod"}}, {"model": "auth.permission", "pk": 6, "fields": {"name": "Can delete pod", "content_type": 2, "codename": "delete_pod"}}, {"model": "auth.permission", "pk": 7, "fields": {"name": "Can add resource", "content_type": 3, "codename": "add_resource"}}, {"model": "auth.permission", "pk": 8, "fields": {"name": "Can change resource", "content_type": 3, "codename": "change_resource"}}, {"model": "auth.permission", "pk": 9, "fields": {"name": "Can delete resource", "content_type": 3, "codename": "delete_resource"}}, {"model": "auth.permission", "pk": 10, "fields": {"name": "Can add server", "content_type": 4, "codename": "add_server"}}, {"model": "auth.permission", "pk": 11, "fields": {"name": "Can change server", "content_type": 4, "codename": "change_server"}}, {"model": "auth.permission", "pk": 12, "fields": {"name": "Can delete server", "content_type": 4, "codename": "delete_server"}}, {"model": "auth.permission", "pk": 13, "fields": {"name": "Can add user resource", "content_type": 5, "codename": "add_userresource"}}, {"model": "auth.permission", "pk": 14, "fields": {"name": "Can change user resource", "content_type": 5, "codename": "change_userresource"}}, {"model": "auth.permission", "pk": 15, "fields": {"name": "Can delete user resource", "content_type": 5, "codename": "delete_userresource"}}, {"model": "auth.permission", "pk": 16, "fields": {"name": "Can add log entry", "content_type": 6, "codename": "add_logentry"}}, {"model": "auth.permission", "pk": 17, "fields": {"name": "Can change log entry", "content_type": 6, "codename": "change_logentry"}}, {"model": "auth.permission", "pk": 18, "fields": {"name": "Can delete log entry", "content_type": 6, "codename": "delete_logentry"}}, {"model": "auth.permission", "pk": 19, "fields": {"name": "Can add permission", "content_type": 7, "codename": "add_permission"}}, {"model": "auth.permission", "pk": 20, "fields": {"name": "Can change permission", "content_type": 7, "codename": "change_permission"}}, {"model": "auth.permission", "pk": 21, "fields": {"name": "Can delete permission", "content_type": 7, "codename": "delete_permission"}}, {"model": "auth.permission", "pk": 22, "fields": {"name": "Can add group", "content_type": 8, "codename": "add_group"}}, {"model": "auth.permission", "pk": 23, "fields": {"name": "Can change group", "content_type": 8, "codename": "change_group"}}, {"model": "auth.permission", "pk": 24, "fields": {"name": "Can delete group", "content_type": 8, "codename": "delete_group"}}, {"model": "auth.permission", "pk": 25, "fields": {"name": "Can add user", "content_type": 9, "codename": "add_user"}}, {"model": "auth.permission", "pk": 26, "fields": {"name": "Can change user", "content_type": 9, "codename": "change_user"}}, {"model": "auth.permission", "pk": 27, "fields": {"name": "Can delete user", "content_type": 9, "codename": "delete_user"}}, {"model": "auth.permission", "pk": 28, "fields": {"name": "Can add content type", "content_type": 10, "codename": "add_contenttype"}}, {"model": "auth.permission", "pk": 29, "fields": {"name": "Can change content type", "content_type": 10, "codename": "change_contenttype"}}, {"model": "auth.permission", "pk": 30, "fields": {"name": "Can delete content type", "content_type": 10, "codename": "delete_contenttype"}}, {"model": "auth.permission", "pk": 31, "fields": {"name": "Can add session", "content_type": 11, "codename": "add_session"}}, {"model": "auth.permission", "pk": 32, "fields": {"name": "Can change session", "content_type": 11, "codename": "change_session"}}, {"model": "auth.permission", "pk": 33, "fields": {"name": "Can delete session", "content_type": 11, "codename": "delete_session"}}, {"model": "auth.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$24000$kcQ5X8WQg1tH$KHynJPYTsoBq7vAipLsP0EZyo3qAu1VGJwaoEQeUz3E=", "last_login": "2016-07-24T12:50:11.954Z", "is_superuser": true, "username": "max", "first_name": "", "last_name": "", "email": "max.breitenfeldt@gmail.com", "is_staff": true, "is_active": true, "date_joined": "2016-07-24T12:49:54.488Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 2, "fields": {"password": "pbkdf2_sha256$24000$6z3xKCpZp0xl$76IaKRzJocbGmGG9JUtPz3is2AjlMEIfb9omiTEn2N8=", "last_login": null, "is_superuser": true, "username": "jose", "first_name": "Jose", "last_name": "Lausuch", "email": "jose.lausuch@ericsson.com", "is_staff": true, "is_active": true, "date_joined": "2016-07-24T12:51:30Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 3, "fields": {"password": "pbkdf2_sha256$24000$466rSOMbdzZk$3TXRz9CRoephCrjNbEa7+nLJ4xwz4W0jvTfs4A69D1o=", "last_login": null, "is_superuser": true, "username": "daniel", "first_name": "Daniel", "last_name": "Smith", "email": "daniel.smith@ericsson.com", "is_staff": true, "is_active": true, "date_joined": "2016-07-24T12:52:23Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 4, "fields": {"password": "pbkdf2_sha256$24000$wjeFIjdiblRl$6ugfj1neDEsaxMQbQ0xV+zg/FKw8ImIDyuPqYWkUxE4=", "last_login": null, "is_superuser": true, "username": "jack", "first_name": "Jack", "last_name": "Morgan", "email": "jack.morgan@intel.com", "is_staff": true, "is_active": true, "date_joined": "2016-07-24T12:53:02Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 5, "fields": {"password": "pbkdf2_sha256$24000$qYMcbKTahNGK$fdPgcQKTKhjSHDZx+C2bymU46HpIl/0n5vbXMpplXWE=", "last_login": null, "is_superuser": true, "username": "fatih", "first_name": "Fatih", "last_name": "Degirmenci", "email": "fatih.degirmenci@ericsson.com", "is_staff": true, "is_active": true, "date_joined": "2016-07-24T12:53:38Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 6, "fields": {"password": "pbkdf2_sha256$24000$Ndxyrota7u3U$KWmjRYo3GW25pcgf9NUuz5PSXvH8hU8E2dUARoKY7EI=", "last_login": null, "is_superuser": true, "username": "trevor", "first_name": "Trevor", "last_name": "Cooper", "email": "trevor.cooper@intel.com", "is_staff": true, "is_active": true, "date_joined": "2016-07-24T12:54:32Z", "groups": [], "user_permissions": []}}, {"model": "admin.logentry", "pk": 1, "fields": {"action_time": "2016-07-24T12:51:30.147Z", "user": 1, "content_type": 9, "object_id": "2", "object_repr": "jose", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 2, "fields": {"action_time": "2016-07-24T12:51:58.928Z", "user": 1, "content_type": 9, "object_id": "2", "object_repr": "jose", "action_flag": 2, "change_message": "Changed first_name, last_name, email, is_staff and is_superuser."}}, {"model": "admin.logentry", "pk": 3, "fields": {"action_time": "2016-07-24T12:52:23.821Z", "user": 1, "content_type": 9, "object_id": "3", "object_repr": "daniel", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 4, "fields": {"action_time": "2016-07-24T12:52:40.446Z", "user": 1, "content_type": 9, "object_id": "3", "object_repr": "daniel", "action_flag": 2, "change_message": "Changed first_name, last_name, email, is_staff and is_superuser."}}, {"model": "admin.logentry", "pk": 5, "fields": {"action_time": "2016-07-24T12:52:45.866Z", "user": 1, "content_type": 9, "object_id": "3", "object_repr": "daniel", "action_flag": 2, "change_message": "No fields changed."}}, {"model": "admin.logentry", "pk": 6, "fields": {"action_time": "2016-07-24T12:53:02.303Z", "user": 1, "content_type": 9, "object_id": "4", "object_repr": "jack", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 7, "fields": {"action_time": "2016-07-24T12:53:22.399Z", "user": 1, "content_type": 9, "object_id": "4", "object_repr": "jack", "action_flag": 2, "change_message": "Changed first_name, last_name, email, is_staff and is_superuser."}}, {"model": "admin.logentry", "pk": 8, "fields": {"action_time": "2016-07-24T12:53:24.541Z", "user": 1, "content_type": 9, "object_id": "4", "object_repr": "jack", "action_flag": 2, "change_message": "No fields changed."}}, {"model": "admin.logentry", "pk": 9, "fields": {"action_time": "2016-07-24T12:53:38.910Z", "user": 1, "content_type": 9, "object_id": "5", "object_repr": "fatih", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 10, "fields": {"action_time": "2016-07-24T12:53:54.585Z", "user": 1, "content_type": 9, "object_id": "5", "object_repr": "fatih", "action_flag": 2, "change_message": "Changed first_name, last_name, email, is_staff and is_superuser."}}, {"model": "admin.logentry", "pk": 11, "fields": {"action_time": "2016-07-24T12:53:56.777Z", "user": 1, "content_type": 9, "object_id": "5", "object_repr": "fatih", "action_flag": 2, "change_message": "No fields changed."}}, {"model": "admin.logentry", "pk": 12, "fields": {"action_time": "2016-07-24T12:54:32.683Z", "user": 1, "content_type": 9, "object_id": "6", "object_repr": "trevor", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 13, "fields": {"action_time": "2016-07-24T12:54:53.188Z", "user": 1, "content_type": 9, "object_id": "6", "object_repr": "trevor", "action_flag": 2, "change_message": "Changed first_name, last_name, email, is_staff and is_superuser."}}, {"model": "admin.logentry", "pk": 14, "fields": {"action_time": "2016-07-24T12:55:45.789Z", "user": 1, "content_type": 5, "object_id": "1", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 15, "fields": {"action_time": "2016-07-24T12:55:51.347Z", "user": 1, "content_type": 5, "object_id": "2", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 16, "fields": {"action_time": "2016-07-24T12:55:56.704Z", "user": 1, "content_type": 5, "object_id": "3", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 17, "fields": {"action_time": "2016-07-24T12:56:08.238Z", "user": 1, "content_type": 5, "object_id": "4", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 18, "fields": {"action_time": "2016-07-24T12:56:17.849Z", "user": 1, "content_type": 5, "object_id": "5", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 19, "fields": {"action_time": "2016-07-24T12:56:24.215Z", "user": 1, "content_type": 5, "object_id": "6", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 20, "fields": {"action_time": "2016-07-24T12:56:33.608Z", "user": 1, "content_type": 5, "object_id": "7", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "admin.logentry", "pk": 21, "fields": {"action_time": "2016-07-24T12:56:37.554Z", "user": 1, "content_type": 5, "object_id": "8", "object_repr": "UserResource object", "action_flag": 1, "change_message": "Added."}}, {"model": "dashboard.userresource", "pk": 1, "fields": {"user": 3, "resource": 108}}, {"model": "dashboard.userresource", "pk": 2, "fields": {"user": 3, "resource": 97}}, {"model": "dashboard.userresource", "pk": 3, "fields": {"user": 4, "resource": 98}}, {"model": "dashboard.userresource", "pk": 4, "fields": {"user": 4, "resource": 103}}, {"model": "dashboard.userresource", "pk": 5, "fields": {"user": 4, "resource": 99}}, {"model": "dashboard.userresource", "pk": 6, "fields": {"user": 4, "resource": 100}}, {"model": "dashboard.userresource", "pk": 7, "fields": {"user": 4, "resource": 101}}, {"model": "dashboard.userresource", "pk": 8, "fields": {"user": 4, "resource": 112}}] \ No newline at end of file
diff --git a/tools/pharos-dashboard/dashboard/fixtures/dashboard.json b/tools/pharos-dashboard/dashboard/fixtures/dashboard.json
new file mode 100644
index 00000000..f0ac3b2f
--- /dev/null
+++ b/tools/pharos-dashboard/dashboard/fixtures/dashboard.json
@@ -0,0 +1,164 @@
+[
+{
+ "model": "dashboard.resource",
+ "pk": 1,
+ "fields": {
+ "name": "Linux Foundation POD 1",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Lf+Lab"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 2,
+ "fields": {
+ "name": "Linux Foundation POD 2",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Lf+Lab"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 3,
+ "fields": {
+ "name": "Ericsson POD 2",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Ericsson+Hosting+and+Request+Process"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 4,
+ "fields": {
+ "name": "Intel POD 2",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod2"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 5,
+ "fields": {
+ "name": "Intel POD 5",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod5"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 6,
+ "fields": {
+ "name": "Intel POD 6",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod6"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 7,
+ "fields": {
+ "name": "Intel POD 8",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod8"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 8,
+ "fields": {
+ "name": "Huawei POD 1",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 9,
+ "fields": {
+ "name": "Intel POD 3",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod3"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 10,
+ "fields": {
+ "name": "Dell POD 1",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Dell+Hosting"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 11,
+ "fields": {
+ "name": "Dell POD 2",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Dell+Hosting"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 12,
+ "fields": {
+ "name": "Orange POD 2",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Opnfv-orange-pod2"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 13,
+ "fields": {
+ "name": "Arm POD 1",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Enea-pharos-lab"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 14,
+ "fields": {
+ "name": "Ericsson POD 1",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Ericsson+Hosting+and+Request+Process"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 15,
+ "fields": {
+ "name": "Huawei POD 2",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 16,
+ "fields": {
+ "name": "Huawei POD 3",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 17,
+ "fields": {
+ "name": "Huawei POD 4",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Huawei+Hosting"
+ }
+},
+{
+ "model": "dashboard.resource",
+ "pk": 18,
+ "fields": {
+ "name": "Intel POD 9",
+ "description": "Some description",
+ "url": "https://wiki.opnfv.org/display/pharos/Intel+Pod9"
+ }
+}
+]
diff --git a/tools/pharos-dashboard/dashboard/forms/booking_form.py b/tools/pharos-dashboard/dashboard/forms/booking_form.py
deleted file mode 100644
index 9cf8048f..00000000
--- a/tools/pharos-dashboard/dashboard/forms/booking_form.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from dashboard.models import Booking
-import django.forms as forms
-from django.utils.translation import ugettext_lazy as _
-
-
-class BookingForm(forms.ModelForm):
- class Meta:
- model = Booking
- fields = ['start_date_time', 'end_date_time', 'purpose', 'booking_id']
-
- PURPOSE = {
- 'id': 'purposefield',
- 'type': 'text',
- 'placeholder': 'Booking purpose',
- }
-
- widgets = {
- 'purpose': forms.TextInput(attrs=PURPOSE),
- }
-
- # DATETIMEFORMAT should be equivalent to the moment.js format string that datetimepicker is
- # using ('YYYY-MM-DD HH:00 ZZ'). The string is used to create a timezone aware datetime object
- DATETIMEFORMAT = '%Y-%m-%d %H:%M %z'
- start_date_time = forms.DateTimeField(input_formats=[DATETIMEFORMAT, ], label='Start')
- end_date_time = forms.DateTimeField(input_formats=[DATETIMEFORMAT, ], label='End')
-
- # we need this to determine if we create a new booking or change an existing booking
- booking_id = forms.IntegerField(widget=forms.HiddenInput, required=False)
-
- def clean(self):
- cleaned_data = super(BookingForm, self).clean()
- if 'start_date_time' not in cleaned_data or 'end_date_time' not in cleaned_data:
- raise forms.ValidationError('Date Missing', code='missing_date')
- if cleaned_data['start_date_time'] >= cleaned_data['end_date_time']:
- raise forms.ValidationError(
- 'Start date is after end date', code='invalid_dates')
- return cleaned_data
diff --git a/tools/pharos-dashboard/dashboard/jenkins/jenkins_util.py b/tools/pharos-dashboard/dashboard/jenkins/jenkins_util.py
deleted file mode 100644
index ba945639..00000000
--- a/tools/pharos-dashboard/dashboard/jenkins/jenkins_util.py
+++ /dev/null
@@ -1,70 +0,0 @@
-import dashboard.jenkins.jenkins_adapter as jenkins
-import re
-
-
-def parse_slave_data(slave_dict, slave):
- slave_dict['status'] = get_slave_status(slave)
- slave_dict['status_color'] = get_status_color(slave)
- slave_dict['slaveurl'] = get_slave_url(slave)
- job = jenkins.get_jenkins_job(slave['displayName'])
- if job is not None:
- slave_dict['last_job'] = parse_job(job)
-
-
-def parse_job(job):
- result = parse_job_string(job['lastBuild']['fullDisplayName'])
- result['url'] = job['url']
- result['color'] = get_job_color(job)
- if job['lastBuild']['building']:
- result['blink'] = 'class=blink_me'
- return result
-
-
-def parse_job_string(full_displayname):
- job = {}
- tokens = re.split(r'[ -]', full_displayname)
- for i in range(len(tokens)):
- if tokens[i] == 'os':
- job['scenario'] = '-'.join(tokens[i: i + 4])
- elif tokens[i] in ['fuel', 'joid', 'apex', 'compass']:
- job['installer'] = tokens[i]
- elif tokens[i] in ['master', 'arno', 'brahmaputra', 'colorado']:
- job['branch'] = tokens[i]
-
- tokens = full_displayname.split(' ')
- job['name'] = tokens[0]
- return job
-
-
-# TODO: use css
-def get_job_color(job):
- if job['lastBuild']['building'] is True:
- return '#646F73'
- result = job['lastBuild']['result']
- if result == 'SUCCESS':
- return '#33cc00'
- if result == 'FAILURE':
- return '#FF5555'
- if result == 'UNSTABLE':
- return '#EDD62B'
-
-
-# TODO: use css
-def get_status_color(slave):
- if not slave['offline'] and slave['idle']:
- return '#C8D6C3'
- if not slave['offline']:
- return '#BEFAAA'
- return '#FAAAAB'
-
-
-def get_slave_url(slave):
- return 'https://build.opnfv.org/ci/computer/' + slave['displayName']
-
-
-def get_slave_status(slave):
- if not slave['offline'] and slave['idle']:
- return 'online / idle'
- if not slave['offline']:
- return 'online'
- return 'offline'
diff --git a/tools/pharos-dashboard/dashboard/migrations/0001_initial.py b/tools/pharos-dashboard/dashboard/migrations/0001_initial.py
deleted file mode 100644
index 12de299e..00000000
--- a/tools/pharos-dashboard/dashboard/migrations/0001_initial.py
+++ /dev/null
@@ -1,107 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.9.8 on 2016-07-24 13:06
-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),
- ]
-
- operations = [
- migrations.CreateModel(
- name='Booking',
- fields=[
- ('booking_id', models.AutoField(primary_key=True, serialize=False)),
- ('start_date_time', models.DateTimeField()),
- ('end_date_time', models.DateTimeField()),
- ('creation', models.DateTimeField(auto_now=True)),
- ('purpose', models.CharField(max_length=300)),
- ],
- options={
- 'db_table': 'booking',
- },
- ),
- migrations.CreateModel(
- name='Pod',
- fields=[
- ('pod_id', models.AutoField(primary_key=True, serialize=False)),
- ('chassis', models.CharField(blank=True, max_length=500, null=True)),
- ],
- options={
- 'db_table': 'pod',
- },
- ),
- migrations.CreateModel(
- name='Resource',
- fields=[
- ('resource_id', models.AutoField(
- primary_key=True, serialize=False)),
- ('name', models.CharField(max_length=100, unique=True)),
- ('slavename', models.CharField(blank=True, max_length=50, null=True)),
- ('description', models.CharField(
- blank=True, max_length=300, null=True)),
- ('url', models.CharField(blank=True, max_length=100, null=True)),
- ('bookable', models.BooleanField(default=False)),
- ('active', models.BooleanField(default=True)),
- ],
- options={
- 'db_table': 'resource',
- },
- ),
- migrations.CreateModel(
- name='Server',
- fields=[
- ('server_id', models.AutoField(primary_key=True, serialize=False)),
- ('model', models.CharField(blank=True, max_length=200, null=True)),
- ('cpu', models.CharField(blank=True, max_length=200, null=True)),
- ('ram', models.CharField(blank=True, max_length=200, null=True)),
- ('storage', models.CharField(blank=True, max_length=200, null=True)),
- ('count', models.IntegerField(default=1)),
- ('resource', models.ForeignKey(
- on_delete=django.db.models.deletion.DO_NOTHING, to='dashboard.Resource')),
- ],
- options={
- 'db_table': 'server',
- },
- ),
- migrations.CreateModel(
- name='UserResource',
- fields=[
- ('user_resource_id', models.AutoField(
- primary_key=True, serialize=False)),
- ('resource', models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE, to='dashboard.Resource')),
- ('user', models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'db_table': 'user_resource',
- },
- ),
- migrations.AddField(
- model_name='pod',
- name='resource',
- field=models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE, to='dashboard.Resource'),
- ),
- 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='user',
- field=models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
- ),
- ]
diff --git a/tools/pharos-dashboard/dashboard/models.py b/tools/pharos-dashboard/dashboard/models.py
index 132ba7c6..d645cd55 100644
--- a/tools/pharos-dashboard/dashboard/models.py
+++ b/tools/pharos-dashboard/dashboard/models.py
@@ -1,63 +1,16 @@
-from __future__ import unicode_literals
-
-from django.db import models
from django.contrib.auth.models import User
+from django.db import models
-
-class Booking(models.Model):
- booking_id = models.AutoField(primary_key=True)
- # Bookings should be deleted before resources
- resource = models.ForeignKey('Resource', models.PROTECT)
- # delete Booking when user is deleted
- user = models.ForeignKey(User, models.CASCADE)
- start_date_time = models.DateTimeField()
- end_date_time = models.DateTimeField()
- creation = models.DateTimeField(auto_now=True)
- purpose = models.CharField(max_length=300)
-
- class Meta:
- db_table = 'booking'
-
- # check for conflicting bookings before saving
- def save(self, *args, **kwargs):
- # conflicts end after booking starts, and start before booking ends
- conflicting_bookings = Booking.objects.filter(resource_id=self.resource_id)
- conflicting_bookings = conflicting_bookings.filter(end_date_time__gt=self.start_date_time)
- conflicting_bookings = conflicting_bookings.filter(start_date_time__lt=self.end_date_time)
- # we may change a booking, so it is not a conflict
- conflicting_bookings = conflicting_bookings.exclude(booking_id=self.booking_id)
- if conflicting_bookings.count() > 0:
- raise ValueError('This booking overlaps with another booking')
- super(Booking, self).save(*args, **kwargs)
-
-
-def __str__(self):
- return 'Booking: ' + str(self.resource)
-
-
-class Pod(models.Model):
- pod_id = models.AutoField(primary_key=True)
- # Delete Pod with resource
- resource = models.ForeignKey('Resource', models.CASCADE)
- chassis = models.CharField(max_length=500, blank=True, null=True)
-
- class Meta:
- db_table = 'pod'
-
- def __str__(self):
- if self.chassis is None:
- return str(self.pod_id) + ' ' + str(self.resource)
- return str(self.pod_id) + ' ' + self.chassis
+from jenkins.models import JenkinsSlave
class Resource(models.Model):
- resource_id = models.AutoField(primary_key=True)
+ id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100, unique=True)
- slavename = models.CharField(max_length=50, blank=True, null=True)
description = models.CharField(max_length=300, blank=True, null=True)
url = models.CharField(max_length=100, blank=True, null=True)
- bookable = models.BooleanField(default=False)
- active = models.BooleanField(default=True)
+ owner = models.ForeignKey(User)
+ slave = models.ForeignKey(JenkinsSlave, on_delete=models.DO_NOTHING, null=True)
class Meta:
db_table = 'resource'
@@ -67,23 +20,16 @@ class Resource(models.Model):
class Server(models.Model):
- server_id = models.AutoField(primary_key=True)
- resource = models.ForeignKey(Resource, models.DO_NOTHING)
- model = models.CharField(max_length=200, blank=True, null=True)
- cpu = models.CharField(max_length=200, blank=True, null=True)
- ram = models.CharField(max_length=200, blank=True, null=True)
- storage = models.CharField(max_length=200, blank=True, null=True)
- count = models.IntegerField(default=1)
+ id = models.AutoField(primary_key=True)
+ resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
+ name = models.CharField(max_length=100, blank=True)
+ model = models.CharField(max_length=100, blank=True)
+ cpu = models.CharField(max_length=100, blank=True)
+ ram = models.CharField(max_length=100, blank=True)
+ storage = models.CharField(max_length=100, blank=True)
class Meta:
db_table = 'server'
-
-class UserResource(models.Model):
- user_resource_id = models.AutoField(primary_key=True)
- user = models.ForeignKey(User, models.CASCADE)
- # Delete if Resource is deleted
- resource = models.ForeignKey(Resource, models.CASCADE)
-
- class Meta:
- db_table = 'user_resource'
+ def __str__(self):
+ return self.name
diff --git a/tools/pharos-dashboard/dashboard/static/css/theme.css b/tools/pharos-dashboard/dashboard/static/css/theme.css
deleted file mode 100644
index 4cec341d..00000000
--- a/tools/pharos-dashboard/dashboard/static/css/theme.css
+++ /dev/null
@@ -1,7 +0,0 @@
-.blink_me {
- animation: blinker 1.5s linear infinite;
-}
-
-@keyframes blinker {
- 20% { opacity: 0.4; }
-} \ No newline at end of file
diff --git a/tools/pharos-dashboard/dashboard/static/js/booking-calendar.js b/tools/pharos-dashboard/dashboard/static/js/booking-calendar.js
deleted file mode 100644
index edc20551..00000000
--- a/tools/pharos-dashboard/dashboard/static/js/booking-calendar.js
+++ /dev/null
@@ -1,68 +0,0 @@
-function parseDisabledTimeIntervals(bookings) {
- var timeIntervals = [];
-
- for (var i = 0; i < bookings.length; i++) {
- var interval = [
- moment(bookings[i]['start_date_time']),
- moment(bookings[i]['end_date_time'])
- ];
- timeIntervals.push(interval);
- }
- return timeIntervals;
-}
-
-function parseCalendarEvents(bookings) {
- var events = [];
- for (var i = 0; i < bookings.length; i++) {
- event = {
- id: bookings[i]['booking_id'],
- title: bookings[i]['purpose'],
- start: bookings[i]['start_date_time'],
- end: bookings[i]['end_date_time'],
- editable: true
- };
- events.push(event);
- }
- return events;
-}
-
-function loadEvents(bookings_url) {
- $.ajax({
- url: bookings_url,
- type: 'get',
- success: function (data) {
- $('#calendar').fullCalendar('addEventSource', parseCalendarEvents(data['bookings']));
- var intervals = parseDisabledTimeIntervals(data['bookings']);
- $('#starttimepicker').data("DateTimePicker").disabledTimeIntervals(intervals);
- $('#endtimepicker').data("DateTimePicker").disabledTimeIntervals(intervals);
- },
- failure: function (data) {
- alert('Error loading booking data');
- }
- });
-}
-
-$(document).ready(function () {
- $('#calendar').fullCalendar(calendarOptions);
- $('#starttimepicker').datetimepicker(timepickerOptions);
- $('#endtimepicker').datetimepicker(timepickerOptions);
-
- loadEvents(bookings_url);
-
- // send Post request to delete url if button is clicked
- $("#deletebutton").click(function () {
- var booking_id = $('#id_booking_id').val();
- $.ajax({
- type: 'post',
- url: '/booking/' + booking_id + '/delete',
- success: function () {
- $('#calendar').fullCalendar('removeEvents');
- loadEvents(bookings_url);
- $('#calendar').fullCalendar('rerenderEvents');
- },
- failure: function () {
- alert('Deleting failed')
- }
- })
- })
-}); \ No newline at end of file
diff --git a/tools/pharos-dashboard/dashboard/static/js/csrf.js b/tools/pharos-dashboard/dashboard/static/js/csrf.js
deleted file mode 100644
index 12429b38..00000000
--- a/tools/pharos-dashboard/dashboard/static/js/csrf.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * use django csrf token in ajax requests
- * source: https://docs.djangoproject.com/en/1.8/ref/csrf/#ajax
- */
-// using jQuery
-function getCookie(name) {
- var cookieValue = null;
- if (document.cookie && document.cookie != '') {
- var cookies = document.cookie.split(';');
- for (var i = 0; i < cookies.length; i++) {
- var cookie = jQuery.trim(cookies[i]);
- // Does this cookie string begin with the name we want?
- if (cookie.substring(0, name.length + 1) == (name + '=')) {
- cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
- break;
- }
- }
- }
- return cookieValue;
-}
-var csrftoken = getCookie('csrftoken');
-
-function csrfSafeMethod(method) {
- // these HTTP methods do not require CSRF protection
- return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
-}
-
-$.ajaxSetup({
- beforeSend: function (xhr, settings) {
- if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
- xhr.setRequestHeader("X-CSRFToken", csrftoken);
- }
- }
-}); \ No newline at end of file
diff --git a/tools/pharos-dashboard/dashboard/static/js/datetimepicker-options.js b/tools/pharos-dashboard/dashboard/static/js/datetimepicker-options.js
deleted file mode 100644
index 1deb8191..00000000
--- a/tools/pharos-dashboard/dashboard/static/js/datetimepicker-options.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * Created by max on 7/25/16.
- */
-var timepickerOptions = {
- format: 'YYYY-MM-DD HH:00 ZZ',
- sideBySide: true
-}; \ No newline at end of file
diff --git a/tools/pharos-dashboard/dashboard/static/js/fullcalendar-options.js b/tools/pharos-dashboard/dashboard/static/js/fullcalendar-options.js
deleted file mode 100644
index c0417eb1..00000000
--- a/tools/pharos-dashboard/dashboard/static/js/fullcalendar-options.js
+++ /dev/null
@@ -1,65 +0,0 @@
-// converts a moment to a readable fomat for the backend
-function convertInputTime(time) {
- return moment(time).format('YYYY-MM-DD HH:00 ZZ');
-}
-
-function sendEventToForm(event) {
- var start = convertInputTime(event.start);
- var end = convertInputTime(event.end);
- $('#starttimepicker').data("DateTimePicker").date(start);
- $('#endtimepicker').data("DateTimePicker").date(end);
- $('#submitform').html("Change Booking");
- $('#purposefield').val(event.title);
- $('#id_booking_id').val(event.id); // create a new booking
- $("#deletebutton").removeClass('hidden'); // show delete button
-}
-
-var calendarOptions = {
- height: 600,
- header: {
- left: 'prev,next today',
- center: 'title',
- right: 'agendaWeek,month'
- },
- timezone: 'local',
- defaultView: 'agendaWeek',
- slotDuration: '00:60:00',
- slotLabelFormat: "HH:mm",
- firstDay: 1,
- allDaySlot: false,
- selectOverlap: false,
- eventOverlap: false,
- selectable: true,
- selectHelper: true,
- editable: false,
- eventLimit: true, // allow "more" link when too many events
- timeFormat: 'H(:mm)', // uppercase H for 24-hour clock
- unselectAuto: false,
-
- select: function (start, end) {
- var start = convertInputTime(start);
- var end = convertInputTime(end);
-
- $('#starttimepicker').data("DateTimePicker").date(start);
- $('#endtimepicker').data("DateTimePicker").date(end);
- $('#submitform').html("Book Pod");
- $('#purposefield').val('');
- $('#id_booking_id').val(''); // create a new booking
- $("#deletebutton").addClass('hidden'); // hide delete button
- },
-
- eventClick: function (event, jsEvent, view) {
- $('#calendar').fullCalendar('unselect');
- sendEventToForm(event);
- },
-
- eventDrop: function (event) {
- $('#calendar').fullCalendar('unselect');
- sendEventToForm(event);
- },
-
- eventResize: function (event) {
- $('#calendar').fullCalendar('unselect');
- sendEventToForm(event);
- }
-}; \ No newline at end of file
diff --git a/tools/pharos-dashboard/dashboard/templatetags/__init__.py b/tools/pharos-dashboard/dashboard/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tools/pharos-dashboard/dashboard/templatetags/__init__.py
diff --git a/tools/pharos-dashboard/dashboard/templatetags/jenkins_filters.py b/tools/pharos-dashboard/dashboard/templatetags/jenkins_filters.py
new file mode 100644
index 00000000..f5038ea5
--- /dev/null
+++ b/tools/pharos-dashboard/dashboard/templatetags/jenkins_filters.py
@@ -0,0 +1,28 @@
+from django.template.defaultfilters import register
+
+
+@register.filter
+def jenkins_job_color(job_result):
+ if job_result == 'SUCCESS':
+ return '#5cb85c'
+ if job_result == 'FAILURE':
+ return '#d9534f'
+ if job_result == 'UNSTABLE':
+ return '#EDD62B'
+ return '#646F73' # job is still building
+
+
+@register.filter
+def jenkins_status_color(slave_status):
+ if slave_status == 'offline':
+ return '#d9534f'
+ if slave_status == 'online':
+ return '#5cb85c'
+ if slave_status == 'online / idle':
+ return '#5bc0de'
+
+
+@register.filter
+def jenkins_job_blink(job_result):
+ if job_result == '': # job is still building
+ return 'class=blink_me'
diff --git a/tools/pharos-dashboard/dashboard/templatetags/jira_filters.py b/tools/pharos-dashboard/dashboard/templatetags/jira_filters.py
new file mode 100644
index 00000000..d9c27612
--- /dev/null
+++ b/tools/pharos-dashboard/dashboard/templatetags/jira_filters.py
@@ -0,0 +1,8 @@
+from django.template.defaultfilters import register
+
+from pharos_dashboard import settings
+
+
+@register.filter
+def jira_issue_url(issue):
+ return settings.JIRA_URL + '/browse/' + str(issue)
diff --git a/tools/pharos-dashboard/dashboard/urls.py b/tools/pharos-dashboard/dashboard/urls.py
index 8050eb88..809204c1 100644
--- a/tools/pharos-dashboard/dashboard/urls.py
+++ b/tools/pharos-dashboard/dashboard/urls.py
@@ -1,35 +1,28 @@
-from dashboard.views.booking import BookingCalendarView, ResourceBookingsView, DeleteBookingView
-from dashboard.views.table_views import CIPodsView, DevelopmentPodsView, JenkinsSlavesView
-from django.conf.urls import url, include
-from django.contrib.auth import views as auth_views
+"""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
-urlpatterns = [
- # registration
- url(r'^accounts/login/$', auth_views.login, name='login'),
- url(r'^accounts/logout/$', auth_views.logout, name='logout'),
-
- # Index
- url(r'^index/$', CIPodsView.as_view(), name='index'),
- url(r'^index/$', CIPodsView.as_view(), name='index'),
- url(r'^$', CIPodsView.as_view(), name=""),
+from dashboard.views import *
- # Tables
+urlpatterns = [
url(r'^ci_pods/$', CIPodsView.as_view(), name='ci_pods'),
url(r'^dev_pods/$', DevelopmentPodsView.as_view(), name='dev_pods'),
url(r'^jenkins_slaves/$', JenkinsSlavesView.as_view(), name='jenkins_slaves'),
+ url(r'^resource/all/$', LabOwnerView.as_view(), name='resources'),
+ url(r'^resource/(?P<resource_id>[0-9]+)/$', ResourceView.as_view(), name='resource'),
- # Booking Calendar
- url(r'^booking_calendar/$', DevelopmentPodsView.as_view(),
- name='booking_calendar'),
- url(r'^booking_calendar/(?P<resource_id>[0-9]+)/$',
- BookingCalendarView.as_view(), name='booking_calendar'),
- url(r'^booking_calendar/(?P<resource_id>[0-9]+)/(?P<booking_id>[0-9]+)/$',
- BookingCalendarView.as_view(), name='booking_calendar'),
-
- # AJAX urls
- url(r'^resource/(?P<resource_id>[0-9]+)/bookings/$',
- ResourceBookingsView.as_view(), name='resource_bookings'),
- url(r'^booking/(?P<booking_id>[0-9]+)/delete$',
- DeleteBookingView.as_view(), name='delete_booking'),
+ url(r'^$', DevelopmentPodsView.as_view(), name="index"),
]
diff --git a/tools/pharos-dashboard/dashboard/views.py b/tools/pharos-dashboard/dashboard/views.py
new file mode 100644
index 00000000..ef1845c8
--- /dev/null
+++ b/tools/pharos-dashboard/dashboard/views.py
@@ -0,0 +1,78 @@
+from datetime import timedelta
+
+from django.shortcuts import get_object_or_404
+from django.utils import timezone
+from django.views.generic import TemplateView
+
+from booking.models import Booking
+from dashboard.models import Resource
+from jenkins.models import JenkinsSlave
+
+
+class JenkinsSlavesView(TemplateView):
+ template_name = "dashboard/jenkins_slaves.html"
+
+ def get_context_data(self, **kwargs):
+ slaves = JenkinsSlave.objects.all()
+ context = super(JenkinsSlavesView, self).get_context_data(**kwargs)
+ context.update({'title': "Jenkins Slaves", 'slaves': slaves})
+ return context
+
+
+class CIPodsView(TemplateView):
+ template_name = "dashboard/ci_pods.html"
+
+ def get_context_data(self, **kwargs):
+ ci_pods = Resource.objects.filter(slave__ci_slave=True)
+ context = super(CIPodsView, self).get_context_data(**kwargs)
+ context.update({'title': "CI Pods", 'ci_pods': ci_pods})
+ return context
+
+
+class DevelopmentPodsView(TemplateView):
+ template_name = "dashboard/dev_pods.html"
+
+ def get_context_data(self, **kwargs):
+ resources = Resource.objects.filter(slave__dev_pod=True)
+
+ bookings = Booking.objects.filter(start__lte=timezone.now())
+ bookings = bookings.filter(end__gt=timezone.now())
+
+ dev_pods = []
+ for resource in resources:
+ dev_pod = (resource, None)
+ for booking in bookings:
+ if booking.resource == resource:
+ dev_pod = (resource, booking)
+ dev_pods.append(dev_pod)
+
+ context = super(DevelopmentPodsView, self).get_context_data(**kwargs)
+ context.update({'title': "Development Pods", 'dev_pods': dev_pods})
+ return context
+
+
+class ResourceView(TemplateView):
+ template_name = "dashboard/resource.html"
+
+ def get_context_data(self, **kwargs):
+ resource = get_object_or_404(Resource, id=self.kwargs['resource_id'])
+ utilization = resource.slave.get_utilization(timedelta(days=7))
+ bookings = Booking.objects.filter(resource=resource, end__gt=timezone.now())
+ context = super(ResourceView, self).get_context_data(**kwargs)
+ context.update({'title': str(resource), 'resource': resource, 'utilization': utilization, 'bookings': bookings})
+ return context
+
+
+class LabOwnerView(TemplateView):
+ template_name = "dashboard/resource_all.html"
+
+ def get_context_data(self, **kwargs):
+ resources = Resource.objects.filter(slave__dev_pod=True)
+ pods = []
+ for resource in resources:
+ utilization = resource.slave.get_utilization(timedelta(days=7))
+ bookings = Booking.objects.filter(resource=resource, end__gt=timezone.now())
+ pods.append((resource, utilization, bookings))
+ context = super(LabOwnerView, self).get_context_data(**kwargs)
+ context.update({'title': "Overview", 'pods': pods})
+ return context
diff --git a/tools/pharos-dashboard/dashboard/views/booking.py b/tools/pharos-dashboard/dashboard/views/booking.py
deleted file mode 100644
index 1499edb7..00000000
--- a/tools/pharos-dashboard/dashboard/views/booking.py
+++ /dev/null
@@ -1,69 +0,0 @@
-from dashboard.forms.booking_form import BookingForm
-from dashboard.models import Resource, Booking
-from dashboard.views.registration import BookingUserTestMixin
-from django.contrib import messages
-from django.contrib.auth.decorators import login_required
-from django.http import Http404, JsonResponse
-from django.shortcuts import get_object_or_404
-from django.utils.decorators import method_decorator
-from django.views.generic import FormView, View
-
-
-@method_decorator(login_required, name='dispatch')
-class BookingCalendarView(BookingUserTestMixin, FormView):
- template_name = "dashboard/booking_calendar.html"
- form_class = BookingForm
-
- # set instance variables
- def dispatch(self, request, *args, **kwargs):
- self.foo = request.GET.get('foo', False)
- self.resource = get_object_or_404(Resource, resource_id=self.kwargs['resource_id'])
- return super(BookingCalendarView, self).dispatch(request, *args, **kwargs)
-
- def form_valid(self, form):
- self.success_url = self.request.path
- booking = None
- # change existing booking
- if form.cleaned_data['booking_id'] is not None:
- booking = get_object_or_404(Booking, booking_id=form.cleaned_data['booking_id'])
- # create new booking
- else:
- booking = Booking()
- booking.resource = self.resource
- booking.user = self.request.user
- booking.start_date_time = form.cleaned_data['start_date_time']
- booking.end_date_time = form.cleaned_data['end_date_time']
- booking.purpose = form.cleaned_data['purpose']
- try:
- booking.save()
- except ValueError:
- messages.add_message(self.request, messages.ERROR,
- 'This booking overlaps with another booking')
- return super(BookingCalendarView, self).form_invalid(form)
- messages.add_message(self.request, messages.SUCCESS,
- 'Booking saved')
- return super(BookingCalendarView, self).form_valid(form)
-
- def get_context_data(self, **kwargs):
- title = 'Booking: ' + self.resource.name
- context = super(BookingCalendarView, self).get_context_data(**kwargs)
- context.update({'title': title, 'resource': self.resource})
- return context
-
-
-@method_decorator(login_required, name='dispatch')
-class ResourceBookingsView(BookingUserTestMixin, View):
- def get(self, request, *args, **kwargs):
- resource = Resource.objects.get(resource_id=self.kwargs['resource_id'])
- bookings = resource.booking_set.get_queryset().values(
- 'booking_id', 'user__username', 'user__email',
- 'start_date_time', 'end_date_time', 'purpose')
- return JsonResponse({'bookings': list(bookings)})
-
-
-@method_decorator(login_required, name='dispatch')
-class DeleteBookingView(BookingUserTestMixin, View):
- def post(self, request, *args, **kwargs):
- booking = get_object_or_404(Booking, booking_id=self.kwargs['booking_id'])
- booking.delete()
- return JsonResponse({True: self.kwargs['booking_id']})
diff --git a/tools/pharos-dashboard/dashboard/views/registration.py b/tools/pharos-dashboard/dashboard/views/registration.py
deleted file mode 100644
index 516fb298..00000000
--- a/tools/pharos-dashboard/dashboard/views/registration.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from django.contrib.auth.mixins import UserPassesTestMixin
-
-
-class BookingUserTestMixin(UserPassesTestMixin):
- # Test if a user has permission to book this Pod
- def test_func(self):
- user = self.request.user
- # Check if User is troubleshooter / admin
- if user.has_perm(('dashboard.add_booking')):
- return True
- # Check if User owns this resource
- user_resources = user.userresource_set.get_queryset()
- for user_resource in user_resources:
- if user_resource.resource_id == self.resource.resource_id:
- return True
- return False
diff --git a/tools/pharos-dashboard/dashboard/views/table_views.py b/tools/pharos-dashboard/dashboard/views/table_views.py
deleted file mode 100644
index 4404234e..00000000
--- a/tools/pharos-dashboard/dashboard/views/table_views.py
+++ /dev/null
@@ -1,62 +0,0 @@
-import dashboard.jenkins.jenkins_util as jenkins_util
-
-import dashboard.jenkins.jenkins_adapter as jenkins
-from dashboard.models import Resource, Booking
-from django.utils import timezone
-from django.views.generic import TemplateView
-
-
-class JenkinsSlavesView(TemplateView):
- template_name = "tables/jenkins_slaves.html"
-
- def get_context_data(self, **kwargs):
- slaves = jenkins.get_all_slaves()
- for slave in slaves:
- jenkins_util.parse_slave_data(slave, slave)
-
- context = super(JenkinsSlavesView, self).get_context_data(**kwargs)
- context.update({'title': "Jenkins Slaves", 'slaves': slaves})
- return context
-
-
-class CIPodsView(TemplateView):
- template_name = "tables/ci_pods.html"
-
- def get_context_data(self, **kwargs):
- resources = Resource.objects.filter().values() # get resources as a set of dicts
- ci_pods = []
- for resource in resources:
- if not jenkins.is_ci_slave(resource['slavename']):
- continue
- ci_slave = jenkins.get_slave(resource['slavename'])
- jenkins_util.parse_slave_data(resource, ci_slave)
- ci_pods.append(resource)
-
- context = super(CIPodsView, self).get_context_data(**kwargs)
- context.update({'title': "CI Pods", 'ci_pods': ci_pods})
- return context
-
-
-class DevelopmentPodsView(TemplateView):
- template_name = "tables/dev_pods.html"
-
- def get_context_data(self, **kwargs):
- resources = Resource.objects.filter().values() # get resources as a set of dicts
- dev_pods = []
-
- current_bookings = Booking.objects.filter(start_date_time__lte=timezone.now())
- current_bookings = current_bookings.filter(end_date_time__gt=timezone.now())
-
- for resource in resources:
- if not jenkins.is_dev_pod(resource['slavename']):
- continue
- dev_pod = jenkins.get_slave(resource['slavename'])
- jenkins_util.parse_slave_data(resource, dev_pod)
- for booking in current_bookings:
- if booking.resource.slavename == resource['slavename']:
- resource['current_booking'] = booking
- dev_pods.append(resource)
-
- context = super(DevelopmentPodsView, self).get_context_data(**kwargs)
- context.update({'title': "Development Pods", 'dev_pods': dev_pods})
- return context
diff --git a/tools/pharos-dashboard/issues.org b/tools/pharos-dashboard/issues.org
deleted file mode 100644
index 0a7d3cae..00000000
--- a/tools/pharos-dashboard/issues.org
+++ /dev/null
@@ -1,6 +0,0 @@
-* Fullcalendar
-- no selectHelper for month select
-- if an event is selected in month select, browser timezone is ignored
-
-* layout
-- datatable does not stay in its panel if zoom is to high / browser window is not maximized
diff --git a/tools/pharos-dashboard/jenkins/__init__.py b/tools/pharos-dashboard/jenkins/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tools/pharos-dashboard/jenkins/__init__.py
diff --git a/tools/pharos-dashboard/dashboard/jenkins/jenkins_adapter.py b/tools/pharos-dashboard/jenkins/adapter.py
index cd848ebb..f9e352a1 100644
--- a/tools/pharos-dashboard/dashboard/jenkins/jenkins_adapter.py
+++ b/tools/pharos-dashboard/jenkins/adapter.py
@@ -1,10 +1,11 @@
-import requests
import logging
+
+import re
+import requests
from django.core.cache import cache
logger = logging.getLogger(__name__)
-
# TODO: implement caching decorator, cache get_* functions
def get_json(url):
if cache.get(url) is None:
@@ -12,7 +13,7 @@ def get_json(url):
response = requests.get(url)
json = response.json()
cache.set(url, json, 180) # cache result for 180 seconds
- return response.json()
+ return json
except requests.exceptions.RequestException as e:
logger.exception(e)
except ValueError as e:
@@ -82,3 +83,42 @@ def is_dev_pod(slavename):
if slavename.find('pod') != -1:
return True
return False
+
+
+def parse_job(job):
+ result = parse_job_string(job['lastBuild']['fullDisplayName'])
+ result['building'] = job['lastBuild']['building']
+ result['result'] = ''
+ if not job['lastBuild']['building']:
+ result['result'] = job['lastBuild']['result']
+ result['url'] = job['url']
+ return result
+
+
+def parse_job_string(full_displayname):
+ job = {}
+ job['scenario'] = ''
+ job['installer'] = ''
+ job['branch'] = ''
+ tokens = re.split(r'[ -]', full_displayname)
+ for i in range(len(tokens)):
+ if tokens[i] == 'os':
+ job['scenario'] = '-'.join(tokens[i: i + 4])
+ elif tokens[i] in ['fuel', 'joid', 'apex', 'compass']:
+ job['installer'] = tokens[i]
+ elif tokens[i] in ['master', 'arno', 'brahmaputra', 'colorado']:
+ job['branch'] = tokens[i]
+ tokens = full_displayname.split(' ')
+ job['name'] = tokens[0]
+ return job
+
+def get_slave_url(slave):
+ return 'https://build.opnfv.org/ci/computer/' + slave['displayName']
+
+
+def get_slave_status(slave):
+ if not slave['offline'] and slave['idle']:
+ return 'online / idle'
+ if not slave['offline']:
+ return 'online'
+ return 'offline'
diff --git a/tools/pharos-dashboard/jenkins/apps.py b/tools/pharos-dashboard/jenkins/apps.py
new file mode 100644
index 00000000..5abd2155
--- /dev/null
+++ b/tools/pharos-dashboard/jenkins/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class JenkinsConfig(AppConfig):
+ name = 'jenkins'
diff --git a/tools/pharos-dashboard/jenkins/migrations/__init__.py b/tools/pharos-dashboard/jenkins/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tools/pharos-dashboard/jenkins/migrations/__init__.py
diff --git a/tools/pharos-dashboard/jenkins/models.py b/tools/pharos-dashboard/jenkins/models.py
new file mode 100644
index 00000000..354887ab
--- /dev/null
+++ b/tools/pharos-dashboard/jenkins/models.py
@@ -0,0 +1,50 @@
+from django.db import models
+from django.utils import timezone
+
+
+class JenkinsSlave(models.Model):
+ id = models.AutoField(primary_key=True)
+ name = models.CharField(max_length=100, unique=True)
+ status = models.CharField(max_length=30, default='offline')
+ url = models.CharField(max_length=1024)
+ ci_slave = models.BooleanField(default=False)
+ dev_pod = models.BooleanField(default=False)
+
+ building = models.BooleanField(default=False)
+
+ last_job_name = models.CharField(max_length=1024, default='')
+ last_job_url = models.CharField(max_length=1024, default='')
+ last_job_scenario = models.CharField(max_length=50, default='')
+ last_job_branch = models.CharField(max_length=50, default='')
+ last_job_installer = models.CharField(max_length=50, default='')
+ last_job_result = models.CharField(max_length=30, default='')
+
+ def get_utilization(self, timedelta):
+ """
+ Return a dictionary containing the count of idle, online and offline measurements in the time from
+ now-timedelta to now
+ """
+ utilization = {'idle': 0, 'online': 0, 'offline': 0}
+ statistics = self.jenkinsstatistic_set.filter(timestamp__gte=timezone.now() - timedelta)
+ utilization['idle'] = statistics.filter(idle=True).count()
+ utilization['online'] = statistics.filter(online=True).count()
+ utilization['offline'] = statistics.filter(offline=True).count()
+ return utilization
+
+ class Meta:
+ db_table = 'jenkins_slave'
+
+ def __str__(self):
+ return self.name
+
+
+class JenkinsStatistic(models.Model):
+ id = models.AutoField(primary_key=True)
+ slave = models.ForeignKey(JenkinsSlave, on_delete=models.CASCADE)
+ offline = models.BooleanField(default=False)
+ idle = models.BooleanField(default=False)
+ online = models.BooleanField(default=False)
+ timestamp = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ db_table = 'jenkins_statistic'
diff --git a/tools/pharos-dashboard/jenkins/tasks.py b/tools/pharos-dashboard/jenkins/tasks.py
new file mode 100644
index 00000000..6998cf3b
--- /dev/null
+++ b/tools/pharos-dashboard/jenkins/tasks.py
@@ -0,0 +1,39 @@
+from celery import shared_task
+
+from jenkins.models import JenkinsSlave, JenkinsStatistic
+from .adapter import *
+
+
+@shared_task
+def sync_jenkins():
+ update_jenkins_slaves()
+
+
+def update_jenkins_slaves():
+ jenkins_slaves = get_all_slaves()
+ for slave in jenkins_slaves:
+ jenkins_slave, created = JenkinsSlave.objects.get_or_create(name=slave['displayName'],
+ url=get_slave_url(slave))
+ jenkins_slave.ci_slave = is_ci_slave(slave['displayName'])
+ jenkins_slave.dev_pod = is_dev_pod(slave['displayName'])
+ jenkins_slave.status = get_slave_status(slave)
+
+ last_job = get_jenkins_job(jenkins_slave.name)
+ if last_job is not None:
+ last_job = parse_job(last_job)
+ jenkins_slave.last_job_name = last_job['name']
+ jenkins_slave.last_job_url = last_job['url']
+ jenkins_slave.last_job_scenario = last_job['scenario']
+ jenkins_slave.last_job_branch = last_job['branch']
+ jenkins_slave.last_job_installer = last_job['installer']
+ jenkins_slave.last_job_result = last_job['result']
+ jenkins_slave.save()
+
+ jenkins_statistic = JenkinsStatistic(slave=jenkins_slave)
+ if jenkins_slave.status == 'online' or jenkins_slave.status == 'building':
+ jenkins_statistic.online = True
+ if jenkins_slave.status == 'offline':
+ jenkins_statistic.offline = True
+ if jenkins_slave.status == 'online / idle':
+ jenkins_statistic.idle = True
+ jenkins_statistic.save()
diff --git a/tools/pharos-dashboard/dashboard/tests.py b/tools/pharos-dashboard/jenkins/tests.py
index 75095782..b1414515 100644
--- a/tools/pharos-dashboard/dashboard/tests.py
+++ b/tools/pharos-dashboard/jenkins/tests.py
@@ -1,5 +1,6 @@
-import dashboard.jenkins.jenkins_adapter as jenkins
-from django.test import SimpleTestCase
+from unittest import TestCase
+
+import jenkins.adapter as jenkins
# Tests that the data we get with the jenkinsadapter contains all the
@@ -8,7 +9,7 @@ from django.test import SimpleTestCase
# - the opnfv jenkins url has changed
# - the jenkins api has changed
# - jenkins is not set up / there is no data
-class JenkinsAdapterTestCase(SimpleTestCase):
+class JenkinsAdapterTestCase(TestCase):
def test_get_all_slaves(self):
slaves = jenkins.get_all_slaves()
self.assertTrue(len(slaves) > 0)
diff --git a/tools/pharos-dashboard/manage.py b/tools/pharos-dashboard/manage.py
index 65e6fc6a..97a5ba4a 100755
--- a/tools/pharos-dashboard/manage.py
+++ b/tools/pharos-dashboard/manage.py
@@ -4,7 +4,19 @@ import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pharos_dashboard.settings")
-
- from django.core.management import execute_from_command_line
-
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError:
+ # The above import may fail for some other reason. Ensure that the
+ # issue is really that Django is missing to avoid masking other
+ # exceptions on Python 2.
+ try:
+ import django
+ except ImportError:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ )
+ raise
execute_from_command_line(sys.argv)
diff --git a/tools/pharos-dashboard/pharos_dashboard/__init__.py b/tools/pharos-dashboard/pharos_dashboard/__init__.py
index e69de29b..b6fc8176 100644
--- a/tools/pharos-dashboard/pharos_dashboard/__init__.py
+++ b/tools/pharos-dashboard/pharos_dashboard/__init__.py
@@ -0,0 +1,3 @@
+# This will make sure the app is always imported when
+# Django starts so that shared_task will use this app.
+from .celery import app as celery_app # noqa
diff --git a/tools/pharos-dashboard/pharos_dashboard/celery.py b/tools/pharos-dashboard/pharos_dashboard/celery.py
new file mode 100644
index 00000000..4cf6a7af
--- /dev/null
+++ b/tools/pharos-dashboard/pharos_dashboard/celery.py
@@ -0,0 +1,20 @@
+import os
+
+from celery import Celery
+
+# set the default Django settings module for the 'celery' program.
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pharos_dashboard.settings')
+
+from django.conf import settings # noqa
+
+app = Celery('pharos_dashboard')
+
+# Using a string here means the worker will not have to
+# pickle the object when using Windows.
+app.config_from_object('django.conf:settings')
+app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
+
+
+@app.task(bind=True)
+def debug_task(self):
+ print('Request: {0!r}'.format(self.request)) \ No newline at end of file
diff --git a/tools/pharos-dashboard/pharos_dashboard/settings.py b/tools/pharos-dashboard/pharos_dashboard/settings.py
deleted file mode 100644
index 2bc94965..00000000
--- a/tools/pharos-dashboard/pharos_dashboard/settings.py
+++ /dev/null
@@ -1,124 +0,0 @@
-"""
-Django settings for opnfvdashboard project.
-
-Generated by 'django-admin startproject' using Django 1.9.7.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/1.9/topics/settings/
-
-For the full list of settings and their values, see
-https://docs.djangoproject.com/en/1.9/ref/settings/
-"""
-
-import os
-
-# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
-BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-
-# Quick-start development settings - unsuitable for production
-# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
-
-# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = '=awtgkzaq@ytwbsp$$n=7=m&9*cm7gci7o-dy07)!x1um=g(gf'
-
-# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = True
-
-ALLOWED_HOSTS = []
-
-# Application definition
-
-INSTALLED_APPS = [
- 'dashboard',
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- 'bootstrap3'
-]
-
-MIDDLEWARE_CLASSES = [
- 'django.middleware.security.SecurityMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
-]
-
-ROOT_URLCONF = 'pharos_dashboard.urls'
-
-TEMPLATES = [
- {
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [os.path.join(BASE_DIR, 'templates')]
- ,
- 'APP_DIRS': True,
- 'OPTIONS': {
- 'context_processors': [
- 'django.template.context_processors.debug',
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
- ],
- },
- },
-]
-
-WSGI_APPLICATION = 'pharos_dashboard.wsgi.application'
-
-# Database
-# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
-
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.postgresql',
- 'NAME': 'pharos_dashboard',
- 'USER': 'opnfv',
- 'PASSWORD': 'opnfvopnfv',
- 'HOST': 'localhost',
- 'PORT': '',
- }
-}
-
-# Password validation
-# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
-
-AUTH_PASSWORD_VALIDATORS = [
- {
- 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
- },
-]
-
-LOGIN_REDIRECT_URL = '/'
-
-# Internationalization
-# https://docs.djangoproject.com/en/1.9/topics/i18n/
-
-LANGUAGE_CODE = 'en-us'
-
-TIME_ZONE = 'UTC'
-
-USE_I18N = True
-
-USE_L10N = True
-
-USE_TZ = True
-
-# Static files (CSS, JavaScript, Images)
-# https://docs.djangoproject.com/en/1.9/howto/static-files/
-
-STATIC_URL = '/static/'
diff --git a/tools/pharos-dashboard/pharos_dashboard/urls.py b/tools/pharos-dashboard/pharos_dashboard/urls.py
index 03b9c256..26ab3677 100644
--- a/tools/pharos-dashboard/pharos_dashboard/urls.py
+++ b/tools/pharos-dashboard/pharos_dashboard/urls.py
@@ -1,7 +1,7 @@
-"""opnfvdashboard URL Configuration
+"""pharos_dashboard URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
- https://docs.djangoproject.com/en/1.9/topics/http/urls/
+ https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
@@ -13,11 +13,13 @@ 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 include, url
+from django.conf.urls import url, include
from django.contrib import admin
urlpatterns = [
url(r'^', include('dashboard.urls', namespace='dashboard')),
- url(r'^admin/', include(admin.site.urls)),
+ url(r'^booking/', include('booking.urls', namespace='booking')),
+ url(r'^account/', include('account.urls', namespace='account')),
+
+ url(r'^admin/', admin.site.urls),
] \ No newline at end of file
diff --git a/tools/pharos-dashboard/pharos_dashboard/wsgi.py b/tools/pharos-dashboard/pharos_dashboard/wsgi.py
index 54f57355..b1277516 100644
--- a/tools/pharos-dashboard/pharos_dashboard/wsgi.py
+++ b/tools/pharos-dashboard/pharos_dashboard/wsgi.py
@@ -4,7 +4,7 @@ WSGI config for pharos_dashboard project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
-https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/
+https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
import os
diff --git a/tools/pharos-dashboard/requirements.txt b/tools/pharos-dashboard/requirements.txt
deleted file mode 100644
index 6306c0b5..00000000
--- a/tools/pharos-dashboard/requirements.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-Django==1.9.8
-django-bootstrap3==7.0.1
-psycopg2==2.6.2
-pytz==2016.6.1
-requests==2.10.0
diff --git a/tools/pharos-dashboard/dashboard/static/bower.json b/tools/pharos-dashboard/static/bower.json
index 78406217..78406217 100644
--- a/tools/pharos-dashboard/dashboard/static/bower.json
+++ b/tools/pharos-dashboard/static/bower.json
diff --git a/tools/pharos-dashboard/static/css/theme.css b/tools/pharos-dashboard/static/css/theme.css
new file mode 100644
index 00000000..bd156372
--- /dev/null
+++ b/tools/pharos-dashboard/static/css/theme.css
@@ -0,0 +1,13 @@
+.blink_me {
+ animation: blinker 1.5s linear infinite;
+}
+
+@keyframes blinker {
+ 20% {
+ opacity: 0.4;
+ }
+}
+
+.modal p {
+ word-wrap: break-word;
+} \ No newline at end of file
diff --git a/tools/pharos-dashboard/static/js/booking-calendar.js b/tools/pharos-dashboard/static/js/booking-calendar.js
new file mode 100644
index 00000000..f8a9a0ff
--- /dev/null
+++ b/tools/pharos-dashboard/static/js/booking-calendar.js
@@ -0,0 +1,36 @@
+function parseCalendarEvents(bookings) {
+ var events = [];
+ for (var i = 0; i < bookings.length; i++) {
+ // convert ISO 8601 timestring to moment, needed for timezone handling
+ start = moment(bookings[i]['start']);
+ end = moment(bookings[i]['end']);
+ event = {
+ id: bookings[i]['id'],
+ title: bookings[i]['purpose'],
+ start: start,
+ end: end,
+ };
+ events.push(event);
+ }
+ return events;
+}
+
+function loadEvents(url) {
+ $.ajax({
+ url: url,
+ type: 'get',
+ success: function (data) {
+ $('#calendar').fullCalendar('addEventSource', parseCalendarEvents(data['bookings']));
+ },
+ failure: function (data) {
+ alert('Error loading booking data');
+ }
+ });
+}
+
+$(document).ready(function () {
+ $('#calendar').fullCalendar(calendarOptions);
+ loadEvents(bookings_url);
+ $('#starttimepicker').datetimepicker(timepickerOptions);
+ $('#endtimepicker').datetimepicker(timepickerOptions);
+}); \ No newline at end of file
diff --git a/tools/pharos-dashboard/dashboard/static/js/dataTables-sort.js b/tools/pharos-dashboard/static/js/dataTables-sort.js
index 7189ca15..7189ca15 100644
--- a/tools/pharos-dashboard/dashboard/static/js/dataTables-sort.js
+++ b/tools/pharos-dashboard/static/js/dataTables-sort.js
diff --git a/tools/pharos-dashboard/static/js/datetimepicker-options.js b/tools/pharos-dashboard/static/js/datetimepicker-options.js
new file mode 100644
index 00000000..2d19ddae
--- /dev/null
+++ b/tools/pharos-dashboard/static/js/datetimepicker-options.js
@@ -0,0 +1,3 @@
+var timepickerOptions = {
+ format: 'MM/DD/YYYY HH:00'
+}; \ No newline at end of file
diff --git a/tools/pharos-dashboard/static/js/fullcalendar-options.js b/tools/pharos-dashboard/static/js/fullcalendar-options.js
new file mode 100644
index 00000000..f4fa50b3
--- /dev/null
+++ b/tools/pharos-dashboard/static/js/fullcalendar-options.js
@@ -0,0 +1,91 @@
+var tmpevent;
+
+function sendEventToForm(event) {
+ $('#starttimepicker').data("DateTimePicker").date(event.start);
+ $('#endtimepicker').data("DateTimePicker").date(event.end);
+}
+
+var calendarOptions = {
+ height: 600,
+ header: {
+ left: 'prev,next today',
+ center: 'title',
+ right: 'agendaWeek,month'
+ },
+ timezone: user_timezone, // set in booking_calendar.html
+ defaultView: 'month',
+ slotDuration: '00:60:00',
+ slotLabelFormat: "HH:mm",
+ firstDay: 1,
+ allDaySlot: false,
+ selectOverlap: false,
+ eventOverlap: false,
+ selectable: true,
+ editable: false,
+ eventLimit: true, // allow "more" link when too many events
+ timeFormat: 'H(:mm)', // uppercase H for 24-hour clock
+ unselectAuto: true,
+ nowIndicator: true,
+
+ // selectHelper is only working in the agendaWeek view, this is a workaround:
+ // if an event is selected, the existing selection is removed and a temporary event is added
+ // to the calendar
+ select: function (start, end) {
+ if (tmpevent != undefined) {
+ $('#calendar').fullCalendar('removeEvents', tmpevent.id);
+ $('#calendar').fullCalendar('rerenderEvents');
+ tmpevent = undefined;
+ }
+ // the times need to be converted here to make them show up in the agendaWeek view if they
+ // are created in the month view. If they are not converted, the tmpevent will only show
+ // up in the (deactivated) allDaySlot
+ start = moment(start);
+ end = moment(end);
+
+ tmpevent = {
+ id: '537818f62bc63518ece15338fb86c8be',
+ title: 'New Booking',
+ start: start,
+ end: end,
+ editable: true
+ };
+
+ $('#calendar').fullCalendar('renderEvent', tmpevent, true);
+ sendEventToForm(tmpevent);
+ },
+
+ eventClick: function (event) {
+ if (tmpevent != undefined) {
+ if (event.id != tmpevent.id) {
+ $('#calendar').fullCalendar('removeEvents', tmpevent.id);
+ $('#calendar').fullCalendar('rerenderEvents');
+ tmpevent = undefined;
+ }
+ }
+
+ // tmpevent is deleted if a real event is clicked, load event details
+ if (tmpevent == undefined) {
+ var booking_detail_url = booking_detail_prefix + event.id;
+
+ $.ajax({
+ url: booking_detail_url,
+ type: 'get',
+ success: function (data) {
+ $('#booking_detail_content').html(data);
+ },
+ failure: function (data) {
+ alert('Error loading booking details');
+ }
+ });
+ $('#booking_detail_modal').modal('show');
+ }
+ },
+
+ eventDrop: function (event) {
+ sendEventToForm(event);
+ },
+
+ eventResize: function (event) {
+ sendEventToForm(event);
+ }
+}; \ No newline at end of file
diff --git a/tools/pharos-dashboard/templates/account/userprofile_update_form.html b/tools/pharos-dashboard/templates/account/userprofile_update_form.html
new file mode 100644
index 00000000..542ea81e
--- /dev/null
+++ b/tools/pharos-dashboard/templates/account/userprofile_update_form.html
@@ -0,0 +1,30 @@
+{% extends "layout.html" %}
+{% load bootstrap3 %}
+
+{% block basecontent %}
+ <div class="container">
+ <div class="row">
+ <div class="col-md-4 col-md-offset-4">
+ {% bootstrap_messages %}
+ <div class="login-panel panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">
+ {{ title }}
+ </h3>
+ </div>
+ <div class="panel-body">
+ <form enctype="multipart/form-data" method="post">
+ {% csrf_token %}
+ {% bootstrap_form form %}
+ {% buttons %}
+ <button type="submit" class="btn btn btn-success">
+ Save
+ </button>
+ {% endbuttons %}
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+{% endblock basecontent %}
diff --git a/tools/pharos-dashboard/templates/dashboard/base.html b/tools/pharos-dashboard/templates/base.html
index 544bf0b3..64174a1f 100644
--- a/tools/pharos-dashboard/templates/dashboard/base.html
+++ b/tools/pharos-dashboard/templates/base.html
@@ -1,4 +1,4 @@
-{% extends "layout/base.html" %}
+{% extends "layout.html" %}
{% load bootstrap3 %}
{% block basecontent %}
@@ -16,7 +16,7 @@
</button>
<a href="https://www.opnfv.org/" class="navbar-left"><img
src="https://www.opnfv.org/sites/all/themes/opnfv/logo.png"></a>
- <a class="navbar-brand" href={% url 'dashboard:' %}>Pharos Dashboard</a>
+ <a class="navbar-brand" href={% url 'dashboard:index' %}>Pharos Dashboard</a>
</div>
<!-- /.navbar-header -->
@@ -27,19 +27,20 @@
</a>
<ul class="dropdown-menu dropdown-user">
{% if user.is_authenticated %}
- <li><a href="#"><i class="fa fa-user fa-fw"></i> User Profile</a>
- </li>
- <li><a href="#"><i class="fa fa-gear fa-fw"></i> Settings</a>
+ <li><a href="{% url 'account:settings' %}"><i
+ class="fa fa-gear fa-fw"></i>
+ Settings</a>
</li>
<li class="divider"></li>
- <li><a href="{% url 'dashboard:logout' %}?next={{ request.path }}"><i
+ <li><a href="{% url 'account:logout' %}?next={{ request.path }}"><i
class="fa fa-sign-out fa-fw"></i>
Logout</a>
</li>
{% else %}
- <li><a href="{% url 'dashboard:login' %}"><i
- class="fa fa-sign-out fa-fw"></i>
- Login</a>
+ <li><a href="{% url 'account:login' %}"><i
+ class="fa fa-sign-in fa-fw"></i>
+ Login with Jira</a>
+ <li>
{% endif %}
</ul>
<!-- /.dropdown-user -->
@@ -53,18 +54,23 @@
<ul class="nav" id="side-menu">
<li>
<a href="{% url 'dashboard:ci_pods' %}"><i
- class="fa fa-table fa-fw"></i>CI-Pods</a>
+ class="fa fa-fw"></i>CI-Pods</a>
</li>
<li>
<a href="{% url 'dashboard:dev_pods' %}"><i
- class="fa fa-table fa-fw"></i>Development
+ class="fa fa-fw"></i>Development
Pods</a>
</li>
<li>
<a href="{% url 'dashboard:jenkins_slaves' %}"><i
- class="fa fa-table fa-fw"></i>Jenkins
+ class="fa fa-fw"></i>Jenkins
Slaves</a>
</li>
+ <li>
+ <a href="{% url 'dashboard:resources' %}"><i
+ class="fa fa-fw"></i>Resources
+ </a>
+ </li>
</ul>
</div>
<!-- /.sidebar-collapse -->
diff --git a/tools/pharos-dashboard/templates/dashboard/booking_calendar.html b/tools/pharos-dashboard/templates/booking/booking_calendar.html
index 0f6bece0..de3e3b3d 100644
--- a/tools/pharos-dashboard/templates/dashboard/booking_calendar.html
+++ b/tools/pharos-dashboard/templates/booking/booking_calendar.html
@@ -1,5 +1,6 @@
-{% extends "dashboard/base.html" %}
+{% extends "dashboard/table.html" %}
{% load staticfiles %}
+
{% load bootstrap3 %}
{% block extrahead %}
@@ -10,62 +11,77 @@
{% endblock extrahead %}
{% block content %}
- <div class="row">
- <div class="col-lg-8">
- <div class="container-fluid">
- <div class="panel panel-default">
- <div class="panel-heading">
- Calendar
- </div>
- <div class="panel-body">
- <div id='calendar'>
- </div>
+ <div class="col-lg-8">
+ <div class="container-fluid">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <i class="fa fa-calendar fa-fw"></i>Calendar
+ </div>
+ <div class="panel-body">
+ <div id='calendar'>
</div>
- <!-- /.panel-body -->
</div>
- <!-- /.panel -->
+ <!-- /.panel-body -->
</div>
+ <!-- /.panel -->
</div>
+ </div>
- <div class="col-lg-4">
- <div class="panel panel-default">
- <div class="panel-heading">
- Booking
- </div>
- <div class="panel-body">
+ <div class="col-lg-4">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <i class="fa fa-edit fa-fw"></i>Booking
+ </div>
+ <div class="panel-body">
+ <div id="booking_form_div">
{% bootstrap_form_errors form type='non_fields' %}
-
- <form method="post" class="form" id="bookingform">
+ <form method="post" action="" class="form" id="bookingform">
{% csrf_token %}
+
<div class='input-group' id='starttimepicker'>
- {% bootstrap_field form.start_date_time addon_after='<span class="glyphicon glyphicon-calendar"></span>' %}
+ {% bootstrap_field form.start addon_after='<span class="glyphicon glyphicon-calendar"></span>' %}
</div>
<div class='input-group' id='endtimepicker'>
- {% bootstrap_field form.end_date_time addon_after='<span class="glyphicon glyphicon-calendar"></span>' %}
+ {% bootstrap_field form.end addon_after='<span class="glyphicon glyphicon-calendar"></span>' %}
</div>
{% bootstrap_field form.purpose %}
- {{ form.booking_id }}
+
{% buttons %}
- <button type="submit" class="btn btn btn-success"
- id="submitform">
- Book Pod
- </button>
- <button type="button" class="btn btn btn-danger hidden"
- id="deletebutton">
- Delete Booking
+ <button type="submit" class="btn btn btn-success">
+ Book
</button>
{% endbuttons %}
</form>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div id="booking_detail_modal" class="modal fade" role="dialog">
+ <div class="modal-dialog">
+ <!-- Modal content-->
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal">&times;</button>
+ <h4 class="modal-title">Booking Detail</h4>
+ </div>
+ <div class="modal-body" id="booking_detail_content">
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
+
</div>
</div>
{% endblock content %}
{% block extrajs %}
<script type="text/javascript">
- var bookings_url = '/resource/' + {{ resource.resource_id }} +'/bookings/';
+ var bookings_url = "{% url 'booking:bookings_json' resource_id=resource.id %}";
+ var booking_detail_prefix = "{% url 'booking:detail_prefix' %}";
+ var user_timezone = "{{ request.user.userprofile.timezone }}"
</script>
<script src={% static "bower_components/moment/moment.js" %}></script>
@@ -74,6 +90,5 @@
src={% static "bower_components/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js" %}></script>
<script src={% static "js/fullcalendar-options.js" %}></script>
<script src={% static "js/datetimepicker-options.js" %}></script>
- <script src={% static "js/csrf.js" %}></script>
<script src={% static "js/booking-calendar.js" %}></script>
{% endblock extrajs %} \ No newline at end of file
diff --git a/tools/pharos-dashboard/templates/booking/booking_detail.html b/tools/pharos-dashboard/templates/booking/booking_detail.html
new file mode 100644
index 00000000..d3f47538
--- /dev/null
+++ b/tools/pharos-dashboard/templates/booking/booking_detail.html
@@ -0,0 +1,26 @@
+{% load jira_filters %}
+
+<p>
+ <b>Resource: </b>
+ <a href="{{ booking.resource.url }}">
+ {{ booking.resource.name }}
+ </a>
+</p>
+<p>
+ <b>User: </b> {{ booking.user.username }}
+</p>
+<p>
+ <b>Start: </b> {{ booking.start }}
+</p>
+<p>
+ <b>End: </b> {{ booking.end }}
+</p>
+<p>
+ <b>Purpose: </b> {{ booking.purpose }}
+</p>
+<p>
+ <b>Jira: </b>
+ <a href="{{ jira_issue | jira_issue_url }}">
+ {{ jira_issue }}
+ </a>
+</p> \ No newline at end of file
diff --git a/tools/pharos-dashboard/templates/booking/booking_table.html b/tools/pharos-dashboard/templates/booking/booking_table.html
new file mode 100644
index 00000000..3d0b7575
--- /dev/null
+++ b/tools/pharos-dashboard/templates/booking/booking_table.html
@@ -0,0 +1,33 @@
+{% load jira_filters %}
+
+
+<thead>
+<tr>
+ <th>User</th>
+ <th>Purpose</th>
+ <th>Start</th>
+ <th>End</th>
+ <th>Jira</th>
+</tr>
+</thead>
+<tbody>
+{% for booking in bookings %}
+ <tr>
+ <th>
+ {{ booking.user.username }}
+ </th>
+ <th>
+ {{ booking.purpose }}
+ </th>
+ <th>
+ {{ booking.start }}
+ </th>
+ <th>
+ {{ booking.end }}
+ </th>
+ <th><a target='_blank'
+ href={{ booking.get_jira_issue | jira_issue_url }}>{{ booking.get_jira_issue }}</a>
+ </th>
+ </tr>
+{% endfor %}
+</tbody> \ No newline at end of file
diff --git a/tools/pharos-dashboard/templates/tables/ci_pods.html b/tools/pharos-dashboard/templates/dashboard/ci_pods.html
index 3889664b..2982a6ff 100644
--- a/tools/pharos-dashboard/templates/tables/ci_pods.html
+++ b/tools/pharos-dashboard/templates/dashboard/ci_pods.html
@@ -1,5 +1,6 @@
{% extends "dashboard/table.html" %}
{% load staticfiles %}
+{% load jenkins_filters %}
{% block table %}
<thead>
@@ -20,28 +21,28 @@
<a target='_blank' href={{ pod.url }}>{{ pod.name }}</a>
</th>
<th>
- <a target='_blank' href={{ pod.slaveurl }}>{{ pod.slavename }}</a>
+ <a target='_blank' href={{ pod.slave.url }}>{{ pod.slave.name }}</a>
</th>
- <th style="background-color:{{ pod.status_color }}">
- {{ pod.status }}
+ <th style="background-color:{{ pod.slave.status | jenkins_status_color }}">
+ {{ pod.slave.status }}
</th>
- <th {{ pod.last_job.blink }}>
- {{ pod.last_job.installer }}
+ <th {{ pod.slave.last_job_result | jenkins_job_blink }}>
+ {{ pod.slave.last_job_installer }}
</th>
- <th {{ pod.last_job.blink }}>
- {{ pod.last_job.scenario }}
+ <th {{ pod.slave.last_job_result | jenkins_job_blink }}>
+ {{ pod.slave.last_job_scenario }}
</th>
- <th {{ pod.last_job.blink }}>
- {{ pod.last_job.branch }}
+ <th {{ pod.slave.last_job_result | jenkins_job_blink }}>
+ {{ pod.slave.last_job_branch }}
</th>
- <th><a {{ pod.last_job.blink }} style="color:{{ pod.last_job.color }}"
- target='_blank'
- href={{ pod.last_job.url }}>{{ pod.last_job.name }}</a>
+ <th><a {{ pod.slave.last_job_result | jenkins_job_blink }}
+ style="color:{{ pod.slave.last_job_result | jenkins_job_color }}"
+ target='_blank'
+ href={{ pod.slave.last_job_url }}>{{ pod.slave.last_job_name }}</a>
</th>
</tr>
{% endfor %}`
</tbody>
- </table>
{% endblock table %}
diff --git a/tools/pharos-dashboard/templates/tables/dev_pods.html b/tools/pharos-dashboard/templates/dashboard/dev_pods.html
index 730aa953..9c84bb91 100644
--- a/tools/pharos-dashboard/templates/tables/dev_pods.html
+++ b/tools/pharos-dashboard/templates/dashboard/dev_pods.html
@@ -1,5 +1,6 @@
{% extends "dashboard/table.html" %}
{% load staticfiles %}
+{% load jenkins_filters %}
{% block table %}
<thead>
@@ -14,28 +15,28 @@
</tr>
</thead>
<tbody>
- {% for pod in dev_pods %}
+ {% for pod, booking in dev_pods %}
<tr>
<th>
- <a target='_blank' href={{ pod.url }}>{{ pod.name }}</a>
+ <a href={% url 'dashboard:resource' resource_id=pod.id %}>{{ pod.name }}</a>
</th>
<th>
- <a target='_blank' href={{ pod.slaveurl }}>{{ pod.slavename }}</a>
+ <a target='_blank' href={{ pod.slave.url }}>{{ pod.slave.name }}</a>
</th>
<th>
- {{ pod.current_booking.user }}
+ {{ booking.user.username }}
</th>
<th>
- {{ pod.current_booking.end_date_time }}
+ {{ booking.end }}
</th>
<th>
- {{ pod.current_booking.purpose }}
+ {{ booking.purpose }}
</th>
- <th style="background-color:{{ pod.status_color }}">
- {{ pod.status }}
+ <th style="background-color:{{ pod.slave.status | jenkins_status_color }}">
+ {{ pod.slave.status }}
</th>
<th>
- <a href='{% url 'dashboard:booking_calendar' %}{{ pod.resource_id }}' class="btn btn-primary">
+ <a href="{% url 'booking:create' resource_id=pod.id %}" class="btn btn-primary">
Book
</a>
</th>
diff --git a/tools/pharos-dashboard/templates/tables/jenkins_slaves.html b/tools/pharos-dashboard/templates/dashboard/jenkins_slaves.html
index 2d011b46..830ed198 100644
--- a/tools/pharos-dashboard/templates/tables/jenkins_slaves.html
+++ b/tools/pharos-dashboard/templates/dashboard/jenkins_slaves.html
@@ -1,6 +1,8 @@
{% extends "dashboard/table.html" %}
{% load staticfiles %}
+{% load jenkins_filters %}
+
{% block table %}
<thead>
<tr>
@@ -13,14 +15,15 @@
{% for slave in slaves %}
<tr>
<th><a target='_blank'
- href={{ slave.slaveurl }}>{{ slave.displayName }}</a>
+ href={{ slave.url }}>{{ slave.name }}</a>
</th>
- <th style="background-color:{{ slave.status_color }}">
+ <th style="background-color:{{ slave.status | jenkins_status_color }}">
{{ slave.status }}
</th>
- <th><a {{ slave.last_job.blink }} style="color:{{ slave.last_job.color }}"
- target="_blank" href={{ slave.last_job.url }}>
- {{ slave.last_job.name }}</a>
+ <th><a {{ slave.last_job_result | jenkins_job_blink }}
+ style="color:{{ slave.last_job_result | jenkins_job_color }}"
+ target="_blank" href={{ slave.last_job_url }}>
+ {{ slave.last_job_name }}</a>
</th>
</tr>
{% endfor %}
diff --git a/tools/pharos-dashboard/templates/dashboard/resource.html b/tools/pharos-dashboard/templates/dashboard/resource.html
new file mode 100644
index 00000000..92d02f66
--- /dev/null
+++ b/tools/pharos-dashboard/templates/dashboard/resource.html
@@ -0,0 +1,58 @@
+{% extends "base.html" %}
+{% load staticfiles %}
+
+{% block extrahead %}
+ <!-- Morris Charts CSS -->
+ <link href="{% static "bower_components/morrisjs/morris.css" %}" rel="stylesheet">
+
+ <!-- DataTables CSS -->
+ <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ rel="stylesheet">
+
+ <!-- DataTables Responsive CSS -->
+ <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}"
+ rel="stylesheet">
+{% endblock extrahead %}
+
+
+{% block content %}
+ {% include "dashboard/resource_detail.html" %}
+{% endblock content %}
+
+
+{% block extrajs %}
+ <!-- DataTables JavaScript -->
+ <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ rel="stylesheet">
+
+ <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
+
+
+
+ <!-- Flot Charts JavaScript -->
+ <script src="{% static "bower_components/flot/excanvas.min.js" %}"></script>
+ <script src="{% static "bower_components/flot/jquery.flot.js" %}"></script>
+ <script src="{% static "bower_components/flot/jquery.flot.pie.js" %}"></script>
+ <script src="{% static "bower_components/flot/jquery.flot.resize.js" %}"></script>
+ <script src="{% static "bower_components/flot/jquery.flot.time.js" %}"></script>
+ <script src="{% static "bower_components/flot.tooltip/js/jquery.flot.tooltip.min.js" %}"></script>
+
+ <script type="text/javascript">
+ $(document).ready(function () {
+ $('#{{ resource.id }}_server_table').DataTable({});
+ $('#{{ resource.id }}_bookings_table').DataTable({});
+
+ $(function () {
+ var plotObj = $.plot($("#{{ resource.id }}_slave_utilization"), data_{{ resource.id }}, {
+ series: {
+ pie: {
+ show: true
+ }
+ }
+ });
+
+ });
+ });
+ </script>
+{% endblock extrajs %} \ No newline at end of file
diff --git a/tools/pharos-dashboard/templates/dashboard/resource_all.html b/tools/pharos-dashboard/templates/dashboard/resource_all.html
new file mode 100644
index 00000000..2078475f
--- /dev/null
+++ b/tools/pharos-dashboard/templates/dashboard/resource_all.html
@@ -0,0 +1,74 @@
+{% extends "base.html" %}
+{% load staticfiles %}
+
+{% block extrahead %}
+ <!-- Morris Charts CSS -->
+ <link href="{% static "bower_components/morrisjs/morris.css" %}" rel="stylesheet">
+
+ <!-- DataTables CSS -->
+ <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ rel="stylesheet">
+
+ <!-- DataTables Responsive CSS -->
+ <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}"
+ rel="stylesheet">
+{% endblock extrahead %}
+
+
+{% block content %}
+ {% for resource, utilization, bookings in pods %}
+ <div class="row">
+ <div class="col-lg-12">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ {{ resource.name }}
+ </div>
+ <div class="panel-body">
+ {% include "dashboard/resource_detail.html" %}
+ </div>
+ </div>
+ </div>
+ </div>
+ {% endfor %}
+{% endblock content %}
+
+
+{% block extrajs %}
+ <!-- DataTables JavaScript -->
+ <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ rel="stylesheet">
+
+ <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
+
+
+
+ <!-- Flot Charts JavaScript -->
+ <script src="{% static "bower_components/flot/excanvas.min.js" %}"></script>
+ <script src="{% static "bower_components/flot/jquery.flot.js" %}"></script>
+ <script src="{% static "bower_components/flot/jquery.flot.pie.js" %}"></script>
+ <script src="{% static "bower_components/flot/jquery.flot.resize.js" %}"></script>
+ <script src="{% static "bower_components/flot/jquery.flot.time.js" %}"></script>
+ <script src="{% static "bower_components/flot.tooltip/js/jquery.flot.tooltip.min.js" %}"></script>
+
+ <script type="text/javascript">
+ $(document).ready(function () {
+ {% for resource, utilization, bookings in pods %}
+
+ $('#{{ resource.id }}_server_table').DataTable({});
+ $('#{{ resource.id }}_bookings_table').DataTable({});
+
+ $(function () {
+ var plotObj = $.plot($("#{{ resource.id }}_slave_utilization"), data_{{ resource.id }}, {
+ series: {
+ pie: {
+ show: true
+ }
+ }
+ });
+
+ });
+ {% endfor %}
+ });
+ </script>
+{% endblock extrajs %} \ No newline at end of file
diff --git a/tools/pharos-dashboard/templates/dashboard/resource_detail.html b/tools/pharos-dashboard/templates/dashboard/resource_detail.html
new file mode 100644
index 00000000..4fba4766
--- /dev/null
+++ b/tools/pharos-dashboard/templates/dashboard/resource_detail.html
@@ -0,0 +1,64 @@
+<div class="row">
+ <div class="col-lg-3">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ Utilization
+ </div>
+ <div class="panel-body">
+ <div class="flot-chart">
+ <div class="flot-chart-content" id="{{ resource.id }}_slave_utilization"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-9">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ Servers
+ </div>
+ <div class="panel-body">
+ <div class="dataTables_wrapper">
+ <table class="table table-striped table-bordered table-hover"
+ id="{{ resource.id }}_server_table" cellspacing="0"
+ width="100%">
+ {% include "dashboard/server_table.html" %}
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+<div class="row">
+ <div class="col-lg-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ Bookings
+ </div>
+ <div class="panel-body">
+ <div class="dataTables_wrapper">
+ <table class="table table-striped table-bordered table-hover"
+ id="{{ resource.id }}_bookings_table" cellspacing="0"
+ width="100%">
+ {% include "booking/booking_table.html" %}
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<script type="text/javascript">
+ var data_{{ resource.id }} = [{
+ label: "Offline",
+ data: {{ utilization.offline }},
+ color: '#d9534f'
+ }, {
+ label: "Online",
+ data: {{ utilization.online }},
+ color: '#5cb85c'
+ }, {
+ label: "Idle",
+ data: {{ utilization.idle }},
+ color: '#5bc0de'
+ }];
+</script> \ No newline at end of file
diff --git a/tools/pharos-dashboard/templates/dashboard/server_table.html b/tools/pharos-dashboard/templates/dashboard/server_table.html
new file mode 100644
index 00000000..d47e5204
--- /dev/null
+++ b/tools/pharos-dashboard/templates/dashboard/server_table.html
@@ -0,0 +1,30 @@
+<thead>
+<tr>
+ <th>Server</th>
+ <th>Model</th>
+ <th>CPU</th>
+ <th>RAM</th>
+ <th>Storage</th>
+</tr>
+</thead>
+<tbody>
+{% for server in resource.server_set.all %}
+ <tr>
+ <th>
+ {{ server.name }}
+ </th>
+ <th>
+ {{ server.model }}
+ </th>
+ <th>
+ {{ server.cpu }}
+ </th>
+ <th>
+ {{ server.ram }}
+ </th>
+ <th>
+ {{ server.storage }}
+ </th>
+ </tr>
+{% endfor %}`
+</tbody> \ No newline at end of file
diff --git a/tools/pharos-dashboard/templates/dashboard/table.html b/tools/pharos-dashboard/templates/dashboard/table.html
index 2d0b82ee..addd5c12 100644
--- a/tools/pharos-dashboard/templates/dashboard/table.html
+++ b/tools/pharos-dashboard/templates/dashboard/table.html
@@ -1,4 +1,4 @@
-{% extends "dashboard/base.html" %}
+{% extends "base.html" %}
{% load staticfiles %}
{% block extrahead %}
diff --git a/tools/pharos-dashboard/templates/layout/base.html b/tools/pharos-dashboard/templates/layout.html
index e9e1cd1f..db9490f5 100644
--- a/tools/pharos-dashboard/templates/layout/base.html
+++ b/tools/pharos-dashboard/templates/layout.html
@@ -50,9 +50,12 @@
{% block basecontent %}
{% endblock basecontent %}
-<!-- jQuery -->
-<script src="{% static "bower_components/jquery/dist/jquery.min.js" %}"></script>
-<script src="{% static "bower_components/jquery-migrate/jquery-migrate.min.js" %}"></script>
+
+<script src="https://code.jquery.com/jquery-2.2.4.min.js"
+ integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
+{#<!-- jQuery -->#}
+{#<script src="{% static "bower_components/jquery/dist/jquery.min.js" %}"></script>#}
+{#<script src="{% static "bower_components/jquery-migrate/jquery-migrate.min.js" %}"></script>#}
{#<script src="https://code.jquery.com/jquery-2.2.0.min.js"></script>#}
<!-- Bootstrap Core JavaScript -->
diff --git a/tools/pharos-dashboard/templates/registration/login.html b/tools/pharos-dashboard/templates/registration/login.html
deleted file mode 100644
index efdcd1fb..00000000
--- a/tools/pharos-dashboard/templates/registration/login.html
+++ /dev/null
@@ -1,61 +0,0 @@
-{% extends "layout/base.html" %}
-
-{% block basecontent %}
- <div class="container">
- <div class="row">
- <div class="col-md-4 col-md-offset-4">
- {% if next %}
- <div class="alert alert-dismissable alert-info">
- <button type="button" class="close" data-dismiss="alert" aria-label="Close">
- <span aria-hidden="true">&times;</span>
- </button>
- {% if user.is_authenticated %}
- Your account doesn't have access to this page. To proceed,
- please login with an account that has access.
- {% else %}
- Please login to see this page.
- {% endif %}
- </div>
- {% endif %}
- {% if form.errors %}
- <div class="alert alert-danger alert-dismissable">
- <button type="button" class="close" data-dismiss="alert" aria-label="Close">
- <span aria-hidden="true">&times;</span>
- </button>
- Your username and password didn't match. Please try again.
- </div>
- {% endif %}
- </div>
- </div>
- <div class="row">
- <div class="col-md-4 col-md-offset-4">
- <div class="login-panel panel panel-default">
- <div class="panel-heading">
- <h3 class="panel-title">
- Login
- </h3>
- </div>
- <div class="panel-body">
- <form method="post" action="{% url 'dashboard:login' %}">
- {% csrf_token %}
- <fieldset>
- <div class="form-group">
- <input class="form-control" placeholder="Username" name="username" type="text"
- autofocus>
- </div>
- <div class="form-group">
- <input class="form-control" placeholder="Password" name="password"
- type="password" value="">
- </div>
- <input type="submit" value="Login" class="btn btn-lg btn-success btn-block"/>
- <input type="hidden" name="next" value="{{ next }}"/>
- </fieldset>
- </form>
- </div>
- </div>
- </div>
- </div>
- </div>
- {# Assumes you setup the password_reset view in your URLconf #}
- {# <p><a href="{% url 'password_reset' %}">Lost password?</a></p>#}
-{% endblock basecontent %}