summaryrefslogtreecommitdiffstats
path: root/pharos-dashboard/src
diff options
context:
space:
mode:
Diffstat (limited to 'pharos-dashboard/src')
-rw-r--r--pharos-dashboard/src/account/__init__.py10
-rw-r--r--pharos-dashboard/src/account/admin.py15
-rw-r--r--pharos-dashboard/src/account/apps.py15
-rw-r--r--pharos-dashboard/src/account/forms.py22
-rw-r--r--pharos-dashboard/src/account/jira_util.py66
-rw-r--r--pharos-dashboard/src/account/middleware.py32
-rw-r--r--pharos-dashboard/src/account/migrations/__init__.py10
-rw-r--r--pharos-dashboard/src/account/models.py30
-rw-r--r--pharos-dashboard/src/account/rsa.pem17
-rw-r--r--pharos-dashboard/src/account/rsa.pub6
-rw-r--r--pharos-dashboard/src/account/tests/__init__.py10
-rw-r--r--pharos-dashboard/src/account/tests/test_general.py50
-rw-r--r--pharos-dashboard/src/account/urls.py36
-rw-r--r--pharos-dashboard/src/account/views.py132
-rw-r--r--pharos-dashboard/src/api/__init__.py10
-rw-r--r--pharos-dashboard/src/api/serializers.py32
-rw-r--r--pharos-dashboard/src/api/urls.py38
-rw-r--r--pharos-dashboard/src/api/views.py33
-rw-r--r--pharos-dashboard/src/booking/__init__.py10
-rw-r--r--pharos-dashboard/src/booking/admin.py15
-rw-r--r--pharos-dashboard/src/booking/apps.py15
-rw-r--r--pharos-dashboard/src/booking/forms.py19
-rw-r--r--pharos-dashboard/src/booking/migrations/__init__.py10
-rw-r--r--pharos-dashboard/src/booking/models.py75
-rw-r--r--pharos-dashboard/src/booking/tests/__init__.py10
-rw-r--r--pharos-dashboard/src/booking/tests/test_models.py107
-rw-r--r--pharos-dashboard/src/booking/tests/test_views.py81
-rw-r--r--pharos-dashboard/src/booking/urls.py37
-rw-r--r--pharos-dashboard/src/booking/views.py113
-rw-r--r--pharos-dashboard/src/dashboard/__init__.py10
-rw-r--r--pharos-dashboard/src/dashboard/admin.py16
-rw-r--r--pharos-dashboard/src/dashboard/apps.py15
-rw-r--r--pharos-dashboard/src/dashboard/fixtures/dashboard.json164
-rw-r--r--pharos-dashboard/src/dashboard/migrations/__init__.py10
-rw-r--r--pharos-dashboard/src/dashboard/models.py81
-rw-r--r--pharos-dashboard/src/dashboard/tasks.py23
-rw-r--r--pharos-dashboard/src/dashboard/templatetags/__init__.py10
-rw-r--r--pharos-dashboard/src/dashboard/templatetags/jenkins_filters.py38
-rw-r--r--pharos-dashboard/src/dashboard/templatetags/jira_filters.py18
-rw-r--r--pharos-dashboard/src/dashboard/urls.py40
-rw-r--r--pharos-dashboard/src/dashboard/views.py143
-rw-r--r--pharos-dashboard/src/jenkins/__init__.py10
-rw-r--r--pharos-dashboard/src/jenkins/adapter.py134
-rw-r--r--pharos-dashboard/src/jenkins/admin.py17
-rw-r--r--pharos-dashboard/src/jenkins/apps.py15
-rw-r--r--pharos-dashboard/src/jenkins/migrations/__init__.py10
-rw-r--r--pharos-dashboard/src/jenkins/models.py60
-rw-r--r--pharos-dashboard/src/jenkins/tasks.py49
-rw-r--r--pharos-dashboard/src/jenkins/tests.py52
-rw-r--r--pharos-dashboard/src/manage.py32
-rw-r--r--pharos-dashboard/src/notification/__init__.py11
-rw-r--r--pharos-dashboard/src/notification/admin.py17
-rw-r--r--pharos-dashboard/src/notification/apps.py18
-rw-r--r--pharos-dashboard/src/notification/migrations/__init__.py10
-rw-r--r--pharos-dashboard/src/notification/models.py32
-rw-r--r--pharos-dashboard/src/notification/signals.py25
-rw-r--r--pharos-dashboard/src/notification/tasks.py33
-rw-r--r--pharos-dashboard/src/notification_framework/__init__.py10
-rw-r--r--pharos-dashboard/src/notification_framework/notification.py114
-rw-r--r--pharos-dashboard/src/pharos_dashboard/__init__.py13
-rw-r--r--pharos-dashboard/src/pharos_dashboard/celery.py30
-rw-r--r--pharos-dashboard/src/pharos_dashboard/settings.py176
-rw-r--r--pharos-dashboard/src/pharos_dashboard/urls.py44
-rw-r--r--pharos-dashboard/src/pharos_dashboard/wsgi.py26
-rw-r--r--pharos-dashboard/src/static/bower.json24
-rw-r--r--pharos-dashboard/src/static/css/theme.css13
-rw-r--r--pharos-dashboard/src/static/js/booking-calendar.js46
-rw-r--r--pharos-dashboard/src/static/js/dataTables-sort.js36
-rw-r--r--pharos-dashboard/src/static/js/datetimepicker-options.js13
-rw-r--r--pharos-dashboard/src/static/js/flot-pie-chart.js30
-rw-r--r--pharos-dashboard/src/static/js/fullcalendar-options.js101
-rw-r--r--pharos-dashboard/src/templates/account/user_list.html46
-rw-r--r--pharos-dashboard/src/templates/account/userprofile_update_form.html30
-rw-r--r--pharos-dashboard/src/templates/base.html104
-rw-r--r--pharos-dashboard/src/templates/booking/booking_calendar.html94
-rw-r--r--pharos-dashboard/src/templates/booking/booking_detail.html26
-rw-r--r--pharos-dashboard/src/templates/booking/booking_table.html33
-rw-r--r--pharos-dashboard/src/templates/dashboard/ci_pods.html60
-rw-r--r--pharos-dashboard/src/templates/dashboard/dev_pods.html69
-rw-r--r--pharos-dashboard/src/templates/dashboard/jenkins_slaves.html45
-rw-r--r--pharos-dashboard/src/templates/dashboard/resource.html58
-rw-r--r--pharos-dashboard/src/templates/dashboard/resource_all.html73
-rw-r--r--pharos-dashboard/src/templates/dashboard/resource_detail.html184
-rw-r--r--pharos-dashboard/src/templates/dashboard/server_table.html30
-rw-r--r--pharos-dashboard/src/templates/dashboard/table.html50
-rw-r--r--pharos-dashboard/src/templates/layout.html73
-rw-r--r--pharos-dashboard/src/templates/rest_framework/api.html9
87 files changed, 3809 insertions, 0 deletions
diff --git a/pharos-dashboard/src/account/__init__.py b/pharos-dashboard/src/account/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/pharos-dashboard/src/account/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
diff --git a/pharos-dashboard/src/account/admin.py b/pharos-dashboard/src/account/admin.py
new file mode 100644
index 0000000..18b2e1a
--- /dev/null
+++ b/pharos-dashboard/src/account/admin.py
@@ -0,0 +1,15 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.contrib import admin
+
+from account.models import UserProfile
+
+admin.site.register(UserProfile) \ No newline at end of file
diff --git a/pharos-dashboard/src/account/apps.py b/pharos-dashboard/src/account/apps.py
new file mode 100644
index 0000000..9814648
--- /dev/null
+++ b/pharos-dashboard/src/account/apps.py
@@ -0,0 +1,15 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.apps import AppConfig
+
+
+class AccountsConfig(AppConfig):
+ name = 'account'
diff --git a/pharos-dashboard/src/account/forms.py b/pharos-dashboard/src/account/forms.py
new file mode 100644
index 0000000..7653e2b
--- /dev/null
+++ b/pharos-dashboard/src/account/forms.py
@@ -0,0 +1,22 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+import django.forms as forms
+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/pharos-dashboard/src/account/jira_util.py b/pharos-dashboard/src/account/jira_util.py
new file mode 100644
index 0000000..c333f8c
--- /dev/null
+++ b/pharos-dashboard/src/account/jira_util.py
@@ -0,0 +1,66 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+import base64
+import os
+
+import oauth2 as oauth
+from jira import JIRA
+from tlslite.utils import keyfactory
+
+from django.conf 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/pharos-dashboard/src/account/middleware.py b/pharos-dashboard/src/account/middleware.py
new file mode 100644
index 0000000..0f1dbd8
--- /dev/null
+++ b/pharos-dashboard/src/account/middleware.py
@@ -0,0 +1,32 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.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/pharos-dashboard/src/account/migrations/__init__.py b/pharos-dashboard/src/account/migrations/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/pharos-dashboard/src/account/migrations/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
diff --git a/pharos-dashboard/src/account/models.py b/pharos-dashboard/src/account/models.py
new file mode 100644
index 0000000..621f669
--- /dev/null
+++ b/pharos-dashboard/src/account/models.py
@@ -0,0 +1,30 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.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/pharos-dashboard/src/account/rsa.pem b/pharos-dashboard/src/account/rsa.pem
new file mode 100644
index 0000000..dbd4eed
--- /dev/null
+++ b/pharos-dashboard/src/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/pharos-dashboard/src/account/rsa.pub b/pharos-dashboard/src/account/rsa.pub
new file mode 100644
index 0000000..cc50e45
--- /dev/null
+++ b/pharos-dashboard/src/account/rsa.pub
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0YjCwIfYoprq/FQO6lb3asXrx
+LlJFuCvtinTF5p0GxvQGu5O3gYytUvtC2JlYzypSRjVxwxrsuRcP3e641SdASwfr
+mzyvIgP08N4S0IFzEURkV1wp/IpH7kH41EtbmUmrXSwfNZsnQRE5SYSOhh+LcK2w
+yQkdgcMv11l4KoBkcwIDAQAB
+-----END PUBLIC KEY-----
diff --git a/pharos-dashboard/src/account/tests/__init__.py b/pharos-dashboard/src/account/tests/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/pharos-dashboard/src/account/tests/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
diff --git a/pharos-dashboard/src/account/tests/test_general.py b/pharos-dashboard/src/account/tests/test_general.py
new file mode 100644
index 0000000..72e7ea1
--- /dev/null
+++ b/pharos-dashboard/src/account/tests/test_general.py
@@ -0,0 +1,50 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.contrib.auth.models import User
+from django.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/pharos-dashboard/src/account/urls.py b/pharos-dashboard/src/account/urls.py
new file mode 100644
index 0000000..3962a0c
--- /dev/null
+++ b/pharos-dashboard/src/account/urls.py
@@ -0,0 +1,36 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+"""pharos_dashboard URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/1.10/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.conf.urls import url, include
+ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
+"""
+from django.conf.urls import url
+
+from 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'),
+ url(r'^users/$', UserListView.as_view(), name='users'),
+]
diff --git a/pharos-dashboard/src/account/views.py b/pharos-dashboard/src/account/views.py
new file mode 100644
index 0000000..3b4269d
--- /dev/null
+++ b/pharos-dashboard/src/account/views.py
@@ -0,0 +1,132 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+import 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 TemplateView
+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 django.conf 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'] + \
+ '&oauth_callback=' + settings.OAUTH_CALLBACK_URL
+ 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
+
+class UserListView(TemplateView):
+ template_name = "account/user_list.html"
+
+ def get_context_data(self, **kwargs):
+ users = User.objects.all()
+ context = super(UserListView, self).get_context_data(**kwargs)
+ context.update({'title': "Dashboard Users", 'users': users})
+ return context
diff --git a/pharos-dashboard/src/api/__init__.py b/pharos-dashboard/src/api/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/pharos-dashboard/src/api/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
diff --git a/pharos-dashboard/src/api/serializers.py b/pharos-dashboard/src/api/serializers.py
new file mode 100644
index 0000000..78e6020
--- /dev/null
+++ b/pharos-dashboard/src/api/serializers.py
@@ -0,0 +1,32 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from rest_framework import serializers
+
+from booking.models import Booking
+from dashboard.models import Server, Resource
+
+
+class BookingSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = Booking
+ fields = ('id', 'resource', 'start', 'end', 'purpose')
+
+
+class ServerSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = Server
+ fields = ('id', 'resource', 'name', 'model', 'cpu', 'ram', 'storage')
+
+
+class ResourceSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = Resource
+ fields = ('id', 'name', 'description', 'url', 'server_set')
diff --git a/pharos-dashboard/src/api/urls.py b/pharos-dashboard/src/api/urls.py
new file mode 100644
index 0000000..5206ac7
--- /dev/null
+++ b/pharos-dashboard/src/api/urls.py
@@ -0,0 +1,38 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+"""pharos_dashboard URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/1.10/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.conf.urls import url, include
+ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
+"""
+from django.conf.urls import url, include
+from rest_framework import routers
+
+from api.views import *
+
+router = routers.DefaultRouter()
+router.register(r'resources', ResourceViewSet)
+router.register(r'servers', ServerViewSet)
+router.register(r'bookings', BookingViewSet)
+
+urlpatterns = [
+ url(r'^', include(router.urls)),
+] \ No newline at end of file
diff --git a/pharos-dashboard/src/api/views.py b/pharos-dashboard/src/api/views.py
new file mode 100644
index 0000000..761ce6e
--- /dev/null
+++ b/pharos-dashboard/src/api/views.py
@@ -0,0 +1,33 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from rest_framework import viewsets
+
+from api.serializers import ResourceSerializer, ServerSerializer, BookingSerializer
+from booking.models import Booking
+from dashboard.models import Resource, Server
+
+
+class BookingViewSet(viewsets.ModelViewSet):
+ queryset = Booking.objects.all()
+ serializer_class = BookingSerializer
+ filter_fields = ('resource', 'user')
+
+
+class ServerViewSet(viewsets.ModelViewSet):
+ queryset = Server.objects.all()
+ serializer_class = ServerSerializer
+ filter_fields = ('resource', 'name')
+
+
+class ResourceViewSet(viewsets.ModelViewSet):
+ queryset = Resource.objects.all()
+ serializer_class = ResourceSerializer
+ filter_fields = ('name',) \ No newline at end of file
diff --git a/pharos-dashboard/src/booking/__init__.py b/pharos-dashboard/src/booking/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/pharos-dashboard/src/booking/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
diff --git a/pharos-dashboard/src/booking/admin.py b/pharos-dashboard/src/booking/admin.py
new file mode 100644
index 0000000..7a7f251
--- /dev/null
+++ b/pharos-dashboard/src/booking/admin.py
@@ -0,0 +1,15 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.contrib import admin
+
+from booking.models import Booking
+
+admin.site.register(Booking) \ No newline at end of file
diff --git a/pharos-dashboard/src/booking/apps.py b/pharos-dashboard/src/booking/apps.py
new file mode 100644
index 0000000..99bf115
--- /dev/null
+++ b/pharos-dashboard/src/booking/apps.py
@@ -0,0 +1,15 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.apps import AppConfig
+
+
+class BookingConfig(AppConfig):
+ name = 'booking'
diff --git a/pharos-dashboard/src/booking/forms.py b/pharos-dashboard/src/booking/forms.py
new file mode 100644
index 0000000..02ac887
--- /dev/null
+++ b/pharos-dashboard/src/booking/forms.py
@@ -0,0 +1,19 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+import django.forms as forms
+
+
+class BookingForm(forms.Form):
+ fields = ['start', 'end', 'purpose']
+
+ start = forms.DateTimeField()
+ end = forms.DateTimeField()
+ purpose = forms.CharField(max_length=300) \ No newline at end of file
diff --git a/pharos-dashboard/src/booking/migrations/__init__.py b/pharos-dashboard/src/booking/migrations/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/pharos-dashboard/src/booking/migrations/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
diff --git a/pharos-dashboard/src/booking/models.py b/pharos-dashboard/src/booking/models.py
new file mode 100644
index 0000000..200dc83
--- /dev/null
+++ b/pharos-dashboard/src/booking/models.py
@@ -0,0 +1,75 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.contrib.auth.models import User
+from django.db import models
+from jira import JIRA
+from jira import JIRAError
+
+from dashboard.models import Resource
+from django.conf import settings
+
+
+class Booking(models.Model):
+ id = models.AutoField(primary_key=True)
+ user = models.ForeignKey(User, models.CASCADE) # delete if user is deleted
+ resource = models.ForeignKey(Resource, models.PROTECT)
+ start = models.DateTimeField()
+ end = models.DateTimeField()
+ jira_issue_id = models.IntegerField(null=True)
+ jira_issue_status = models.CharField(max_length=50)
+
+ purpose = models.CharField(max_length=300, blank=False)
+
+ class Meta:
+ db_table = 'booking'
+
+ def get_jira_issue(self):
+ try:
+ jira = JIRA(server=settings.JIRA_URL,
+ basic_auth=(settings.JIRA_USER_NAME, settings.JIRA_USER_PASSWORD))
+ issue = jira.issue(self.jira_issue_id)
+ return issue
+ except JIRAError:
+ return None
+
+ def authorization_test(self):
+ """
+ Return True if self.user is authorized to make this booking.
+ """
+ user = self.user
+ # Check if User is troubleshooter / admin
+ if user.has_perm('booking.add_booking'):
+ return True
+ # Check if User owns this resource
+ if user == self.resource.owner:
+ return True
+ return False
+
+ def save(self, *args, **kwargs):
+ """
+ Save the booking if self.user is authorized and there is no overlapping booking.
+ Raise PermissionError if the user is not authorized
+ Raise ValueError if there is an overlapping booking
+ """
+ if not self.authorization_test():
+ raise PermissionError('Insufficient permissions to save this booking.')
+ if self.start >= self.end:
+ raise ValueError('Start date is after end date')
+ # conflicts end after booking starts, and start before booking ends
+ conflicting_dates = Booking.objects.filter(resource=self.resource).exclude(id=self.id)
+ conflicting_dates = conflicting_dates.filter(end__gt=self.start)
+ conflicting_dates = conflicting_dates.filter(start__lt=self.end)
+ if conflicting_dates.count() > 0:
+ raise ValueError('This booking overlaps with another booking')
+ return super(Booking, self).save(*args, **kwargs)
+
+ def __str__(self):
+ return str(self.resource) + ' from ' + str(self.start) + ' until ' + str(self.end)
diff --git a/pharos-dashboard/src/booking/tests/__init__.py b/pharos-dashboard/src/booking/tests/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/pharos-dashboard/src/booking/tests/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
diff --git a/pharos-dashboard/src/booking/tests/test_models.py b/pharos-dashboard/src/booking/tests/test_models.py
new file mode 100644
index 0000000..612b35c
--- /dev/null
+++ b/pharos-dashboard/src/booking/tests/test_models.py
@@ -0,0 +1,107 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from datetime import timedelta
+
+from django.contrib.auth.models import User, Permission
+from django.test import TestCase
+from django.utils import timezone
+
+from account.models import UserProfile
+from booking.models import Booking
+from dashboard.models import Resource
+from jenkins.models import JenkinsSlave
+
+
+class BookingModelTestCase(TestCase):
+ def setUp(self):
+ self.slave = JenkinsSlave.objects.create(name='test', url='test')
+ self.owner = User.objects.create(username='owner')
+
+ self.res1 = Resource.objects.create(name='res1', slave=self.slave, description='x',
+ url='x',owner=self.owner)
+ self.res2 = Resource.objects.create(name='res2', slave=self.slave, description='x',
+ url='x',owner=self.owner)
+
+ self.user1 = User.objects.create(username='user1')
+
+ self.add_booking_perm = Permission.objects.get(codename='add_booking')
+ self.user1.user_permissions.add(self.add_booking_perm)
+
+ self.user1 = User.objects.get(pk=self.user1.id)
+
+ def test_start__end(self):
+ """
+ if the start of a booking is greater or equal then the end, saving should raise a
+ ValueException
+ """
+ start = timezone.now()
+ end = start - timedelta(weeks=1)
+ self.assertRaises(ValueError, Booking.objects.create, start=start, end=end,
+ resource=self.res1, user=self.user1)
+ end = start
+ self.assertRaises(ValueError, Booking.objects.create, start=start, end=end,
+ resource=self.res1, user=self.user1)
+
+ def test_conflicts(self):
+ """
+ saving an overlapping booking on the same resource should raise a ValueException
+ saving for different resources should succeed
+ """
+ start = timezone.now()
+ end = start + timedelta(weeks=1)
+ self.assertTrue(
+ Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1))
+
+ self.assertRaises(ValueError, Booking.objects.create, start=start,
+ end=end, resource=self.res1, user=self.user1)
+ self.assertRaises(ValueError, Booking.objects.create, start=start + timedelta(days=1),
+ end=end - timedelta(days=1), resource=self.res1, user=self.user1)
+
+ self.assertRaises(ValueError, Booking.objects.create, start=start - timedelta(days=1),
+ end=end, resource=self.res1, user=self.user1)
+ self.assertRaises(ValueError, Booking.objects.create, start=start - timedelta(days=1),
+ end=end - timedelta(days=1), resource=self.res1, user=self.user1)
+
+ self.assertRaises(ValueError, Booking.objects.create, start=start,
+ end=end + timedelta(days=1), resource=self.res1, user=self.user1)
+ self.assertRaises(ValueError, Booking.objects.create, start=start + timedelta(days=1),
+ end=end + timedelta(days=1), resource=self.res1, user=self.user1)
+
+ self.assertTrue(Booking.objects.create(start=start - timedelta(days=1), end=start,
+ user=self.user1, resource=self.res1))
+ self.assertTrue(Booking.objects.create(start=end, end=end + timedelta(days=1),
+ user=self.user1, resource=self.res1))
+
+ self.assertTrue(
+ Booking.objects.create(start=start - timedelta(days=2), end=start - timedelta(days=1),
+ user=self.user1, resource=self.res1))
+ self.assertTrue(
+ Booking.objects.create(start=end + timedelta(days=1), end=end + timedelta(days=2),
+ user=self.user1, resource=self.res1))
+ self.assertTrue(
+ Booking.objects.create(start=start, end=end,
+ user=self.user1, resource=self.res2))
+
+ def test_authorization(self):
+ user = User.objects.create(username='user')
+ user.userprofile = UserProfile.objects.create(user=user)
+ self.assertRaises(PermissionError, Booking.objects.create, start=timezone.now(),
+ end=timezone.now() + timedelta(days=1), resource=self.res1, user=user)
+ self.res1.owner = user
+ self.assertTrue(
+ Booking.objects.create(start=timezone.now(), end=timezone.now() + timedelta(days=1),
+ resource=self.res1, user=user))
+ self.res1.owner = self.owner
+ user.user_permissions.add(self.add_booking_perm)
+ user = User.objects.get(pk=user.id)
+ self.assertTrue(
+ Booking.objects.create(start=timezone.now(), end=timezone.now() + timedelta(days=1),
+ resource=self.res2, user=user))
diff --git a/pharos-dashboard/src/booking/tests/test_views.py b/pharos-dashboard/src/booking/tests/test_views.py
new file mode 100644
index 0000000..e568c15
--- /dev/null
+++ b/pharos-dashboard/src/booking/tests/test_views.py
@@ -0,0 +1,81 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from datetime import timedelta
+
+from django.contrib import auth
+from django.test import Client
+from django.utils import timezone
+from django.contrib.auth.models import Permission
+from django.test import TestCase
+from django.urls import reverse
+from django.utils.encoding import force_text
+from registration.forms import User
+
+from account.models import UserProfile
+from booking.models import Booking
+from dashboard.models import Resource
+from jenkins.models import JenkinsSlave
+
+
+class BookingViewTestCase(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.slave = JenkinsSlave.objects.create(name='test', url='test')
+ self.owner = User.objects.create(username='owner')
+ self.res1 = Resource.objects.create(name='res1', slave=self.slave, description='x',
+ url='x',owner=self.owner)
+ self.user1 = User.objects.create(username='user1')
+ self.user1.set_password('user1')
+ self.user1profile = UserProfile.objects.create(user=self.user1)
+ self.user1.save()
+
+ self.add_booking_perm = Permission.objects.get(codename='add_booking')
+ self.user1.user_permissions.add(self.add_booking_perm)
+
+ self.user1 = User.objects.get(pk=self.user1.id)
+
+
+ def test_resource_bookings_json(self):
+ url = reverse('booking:bookings_json', kwargs={'resource_id': 0})
+ self.assertEqual(self.client.get(url).status_code, 404)
+
+ url = reverse('booking:bookings_json', kwargs={'resource_id': self.res1.id})
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertJSONEqual(force_text(response.content), {"bookings": []})
+ booking1 = Booking.objects.create(start=timezone.now(),
+ end=timezone.now() + timedelta(weeks=1), user=self.user1,
+ resource=self.res1)
+ response = self.client.get(url)
+ json = response.json()
+ self.assertEqual(response.status_code, 200)
+ self.assertIn('bookings', json)
+ self.assertEqual(len(json['bookings']), 1)
+ self.assertIn('start', json['bookings'][0])
+ self.assertIn('end', json['bookings'][0])
+ self.assertIn('id', json['bookings'][0])
+ self.assertIn('purpose', json['bookings'][0])
+
+ def test_booking_form_view(self):
+ url = reverse('booking:create', kwargs={'resource_id': 0})
+ self.assertEqual(self.client.get(url).status_code, 404)
+
+ # authenticated user
+ url = reverse('booking:create', kwargs={'resource_id': self.res1.id})
+ self.client.login(username='user1',password='user1')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed('booking/booking_calendar.html')
+ self.assertTemplateUsed('booking/booking_form.html')
+ self.assertIn('resource', response.context)
+
+
+
diff --git a/pharos-dashboard/src/booking/urls.py b/pharos-dashboard/src/booking/urls.py
new file mode 100644
index 0000000..5320623
--- /dev/null
+++ b/pharos-dashboard/src/booking/urls.py
@@ -0,0 +1,37 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+"""pharos_dashboard URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/1.10/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.conf.urls import url, include
+ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
+"""
+from django.conf.urls import url
+
+from booking.views import *
+
+urlpatterns = [
+ url(r'^(?P<resource_id>[0-9]+)/$', BookingFormView.as_view(), name='create'),
+ url(r'^(?P<resource_id>[0-9]+)/bookings_json/$', ResourceBookingsJSON.as_view(),
+ name='bookings_json'),
+
+ url(r'^detail/$', BookingView.as_view(), name='detail_prefix'),
+ url(r'^detail/(?P<booking_id>[0-9]+)/$', BookingView.as_view(), name='detail'),
+]
diff --git a/pharos-dashboard/src/booking/views.py b/pharos-dashboard/src/booking/views.py
new file mode 100644
index 0000000..2fe167a
--- /dev/null
+++ b/pharos-dashboard/src/booking/views.py
@@ -0,0 +1,113 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from datetime import timedelta
+
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.http import JsonResponse
+from django.shortcuts import get_object_or_404
+from django.shortcuts import redirect
+from django.urls import reverse
+from django.utils import timezone
+from django.views import View
+from django.views.generic import FormView
+from django.views.generic import TemplateView
+from jira import JIRAError
+
+from account.jira_util import get_jira
+from booking.forms import BookingForm
+from booking.models import Booking
+from dashboard.models import Resource
+
+
+def create_jira_ticket(user, booking):
+ jira = get_jira(user)
+ issue_dict = {
+ 'project': 'PHAROS',
+ 'summary': str(booking.resource) + ': Access Request',
+ 'description': booking.purpose,
+ 'issuetype': {'name': 'Task'},
+ 'components': [{'name': 'POD Access Request'}],
+ 'assignee': {'name': booking.resource.owner.username}
+ }
+ issue = jira.create_issue(fields=issue_dict)
+ jira.add_attachment(issue, user.userprofile.pgp_public_key)
+ jira.add_attachment(issue, user.userprofile.ssh_public_key)
+ booking.jira_issue_id = issue.id
+ booking.save()
+
+
+class BookingFormView(LoginRequiredMixin, FormView):
+ template_name = "booking/booking_calendar.html"
+ form_class = BookingForm
+
+ def dispatch(self, request, *args, **kwargs):
+ self.resource = get_object_or_404(Resource, id=self.kwargs['resource_id'])
+ return super(BookingFormView, self).dispatch(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ title = 'Booking: ' + self.resource.name
+ context = super(BookingFormView, self).get_context_data(**kwargs)
+ context.update({'title': title, 'resource': self.resource})
+ return context
+
+ def get_success_url(self):
+ return reverse('booking:create', kwargs=self.kwargs)
+
+ def form_valid(self, form):
+ user = self.request.user
+ if not user.userprofile.ssh_public_key or not user.userprofile.pgp_public_key:
+ messages.add_message(self.request, messages.INFO,
+ 'Please upload your private keys before booking')
+ return redirect('account:settings')
+ booking = Booking(start=form.cleaned_data['start'], end=form.cleaned_data['end'],
+ purpose=form.cleaned_data['purpose'], resource=self.resource,
+ user=user)
+ try:
+ booking.save()
+ except ValueError as err:
+ messages.add_message(self.request, messages.ERROR, err)
+ return super(BookingFormView, self).form_invalid(form)
+ except PermissionError as err:
+ messages.add_message(self.request, messages.ERROR, err)
+ return super(BookingFormView, self).form_invalid(form)
+ try:
+ if settings.CREATE_JIRA_TICKET:
+ create_jira_ticket(user, booking)
+ except JIRAError:
+ messages.add_message(self.request, messages.ERROR, 'Failed to create Jira Ticket. '
+ 'Please check your Jira '
+ 'permissions.')
+ booking.delete()
+ return super(BookingFormView, self).form_invalid(form)
+ messages.add_message(self.request, messages.SUCCESS, 'Booking saved')
+ return super(BookingFormView, self).form_valid(form)
+
+
+class BookingView(TemplateView):
+ template_name = "booking/booking_detail.html"
+
+ def get_context_data(self, **kwargs):
+ booking = get_object_or_404(Booking, id=self.kwargs['booking_id'])
+ jira_issue = booking.get_jira_issue()
+ title = 'Booking Details'
+ context = super(BookingView, self).get_context_data(**kwargs)
+ context.update({'title': title, 'booking': booking, 'jira_issue': jira_issue})
+ return context
+
+
+class ResourceBookingsJSON(View):
+ def get(self, request, *args, **kwargs):
+ resource = get_object_or_404(Resource, id=self.kwargs['resource_id'])
+ bookings = resource.booking_set.get_queryset().values('id', 'start', 'end', 'purpose',
+ 'jira_issue_status')
+ return JsonResponse({'bookings': list(bookings)}) \ No newline at end of file
diff --git a/pharos-dashboard/src/dashboard/__init__.py b/pharos-dashboard/src/dashboard/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/pharos-dashboard/src/dashboard/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
diff --git a/pharos-dashboard/src/dashboard/admin.py b/pharos-dashboard/src/dashboard/admin.py
new file mode 100644
index 0000000..a1463a7
--- /dev/null
+++ b/pharos-dashboard/src/dashboard/admin.py
@@ -0,0 +1,16 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.contrib import admin
+
+from dashboard.models import *
+
+admin.site.register(Resource)
+admin.site.register(Server)
diff --git a/pharos-dashboard/src/dashboard/apps.py b/pharos-dashboard/src/dashboard/apps.py
new file mode 100644
index 0000000..e0c4f44
--- /dev/null
+++ b/pharos-dashboard/src/dashboard/apps.py
@@ -0,0 +1,15 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.apps import AppConfig
+
+
+class DashboardConfig(AppConfig):
+ name = 'dashboard'
diff --git a/pharos-dashboard/src/dashboard/fixtures/dashboard.json b/pharos-dashboard/src/dashboard/fixtures/dashboard.json
new file mode 100644
index 0000000..f0ac3b2
--- /dev/null
+++ b/pharos-dashboard/src/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/pharos-dashboard/src/dashboard/migrations/__init__.py b/pharos-dashboard/src/dashboard/migrations/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/pharos-dashboard/src/dashboard/migrations/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
diff --git a/pharos-dashboard/src/dashboard/models.py b/pharos-dashboard/src/dashboard/models.py
new file mode 100644
index 0000000..050834e
--- /dev/null
+++ b/pharos-dashboard/src/dashboard/models.py
@@ -0,0 +1,81 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from datetime import timedelta
+
+from django.utils import timezone
+from django.contrib.auth.models import User
+from django.db import models
+
+from jenkins.models import JenkinsSlave
+
+
+class Resource(models.Model):
+ id = models.AutoField(primary_key=True)
+ name = models.CharField(max_length=100, unique=True)
+ description = models.CharField(max_length=300, blank=True, null=True)
+ url = models.CharField(max_length=100, blank=True, null=True)
+ owner = models.ForeignKey(User, related_name='user_lab_owner', null=True)
+ vpn_users = models.ManyToManyField(User, related_name='user_vpn_users')
+ slave = models.ForeignKey(JenkinsSlave, on_delete=models.DO_NOTHING, null=True)
+
+ def get_booking_utilization(self, weeks):
+ """
+ Return a dictionary containing the count of booked and free seconds for a resource in the
+ range [now,now + weeks] if weeks is positive,
+ or [now-weeks, now] if weeks is negative
+ """
+
+ length = timedelta(weeks=abs(weeks))
+ now = timezone.now()
+
+ start = now
+ end = now + length
+ if weeks < 0:
+ start = now - length
+ end = now
+
+ bookings = self.booking_set.filter(start__lt=start + length, end__gt=start)
+
+ booked_seconds = 0
+ for booking in bookings:
+ booking_start = booking.start
+ booking_end = booking.end
+ if booking_start < start:
+ booking_start = start
+ if booking_end > end:
+ booking_end = start + length
+ total = booking_end - booking_start
+ booked_seconds += total.total_seconds()
+
+ return {'booked_seconds': booked_seconds,
+ 'available_seconds': length.total_seconds() - booked_seconds}
+
+ class Meta:
+ db_table = 'resource'
+
+ def __str__(self):
+ return self.name
+
+
+class Server(models.Model):
+ 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'
+
+ def __str__(self):
+ return self.name
diff --git a/pharos-dashboard/src/dashboard/tasks.py b/pharos-dashboard/src/dashboard/tasks.py
new file mode 100644
index 0000000..4c09bf9
--- /dev/null
+++ b/pharos-dashboard/src/dashboard/tasks.py
@@ -0,0 +1,23 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from celery import shared_task
+from datetime import timedelta
+from django.utils import timezone
+
+from jenkins.models import JenkinsStatistic
+from notification.models import BookingNotification
+
+
+@shared_task
+def database_cleanup():
+ now = timezone.now()
+ JenkinsStatistic.objects.filter(timestamp__lt=now - timedelta(weeks=4)).delete()
+ BookingNotification.objects.filter(submit_time__lt=now - timedelta(weeks=4)).delete() \ No newline at end of file
diff --git a/pharos-dashboard/src/dashboard/templatetags/__init__.py b/pharos-dashboard/src/dashboard/templatetags/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/pharos-dashboard/src/dashboard/templatetags/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
diff --git a/pharos-dashboard/src/dashboard/templatetags/jenkins_filters.py b/pharos-dashboard/src/dashboard/templatetags/jenkins_filters.py
new file mode 100644
index 0000000..e7e1425
--- /dev/null
+++ b/pharos-dashboard/src/dashboard/templatetags/jenkins_filters.py
@@ -0,0 +1,38 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.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/pharos-dashboard/src/dashboard/templatetags/jira_filters.py b/pharos-dashboard/src/dashboard/templatetags/jira_filters.py
new file mode 100644
index 0000000..7020843
--- /dev/null
+++ b/pharos-dashboard/src/dashboard/templatetags/jira_filters.py
@@ -0,0 +1,18 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.template.defaultfilters import register
+
+from django.conf import settings
+
+
+@register.filter
+def jira_issue_url(issue):
+ return settings.JIRA_URL + '/browse/' + str(issue)
diff --git a/pharos-dashboard/src/dashboard/urls.py b/pharos-dashboard/src/dashboard/urls.py
new file mode 100644
index 0000000..f04f5ca
--- /dev/null
+++ b/pharos-dashboard/src/dashboard/urls.py
@@ -0,0 +1,40 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+"""pharos_dashboard URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/1.10/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.conf.urls import url, include
+ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
+"""
+from django.conf.urls import url
+from dashboard.views import *
+
+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'),
+ url(r'^resource/(?P<resource_id>[0-9]+)/booking_utilization/(?P<weeks>-?\d+)/$',
+ BookingUtilizationJSON.as_view(), name='booking_utilization'),
+ url(r'^resource/(?P<resource_id>[0-9]+)/jenkins_utilization/(?P<weeks>-?\d+)/$',
+ JenkinsUtilizationJSON.as_view(), name='jenkins_utilization'),
+ url(r'^$', DevelopmentPodsView.as_view(), name="index"),
+]
diff --git a/pharos-dashboard/src/dashboard/views.py b/pharos-dashboard/src/dashboard/views.py
new file mode 100644
index 0000000..022a4af
--- /dev/null
+++ b/pharos-dashboard/src/dashboard/views.py
@@ -0,0 +1,143 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from datetime import timedelta
+
+from django.http import JsonResponse
+from django.shortcuts import get_object_or_404
+from django.utils import timezone
+from django.views import View
+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:
+ booking_utilization = resource.get_booking_utilization(weeks=4)
+ total = booking_utilization['booked_seconds'] + booking_utilization['available_seconds']
+ try:
+ utilization_percentage = "%d%%" % (float(booking_utilization['booked_seconds']) /
+ total * 100)
+ except (ValueError, ZeroDivisionError):
+ return ""
+
+ dev_pod = (resource, None, utilization_percentage)
+ for booking in bookings:
+ if booking.resource == resource:
+ dev_pod = (resource, booking, utilization_percentage)
+ 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
+
+
+class BookingUtilizationJSON(View):
+ def get(self, request, *args, **kwargs):
+ resource = get_object_or_404(Resource, id=kwargs['resource_id'])
+ utilization = resource.get_booking_utilization(int(kwargs['weeks']))
+ utilization = [
+ {
+ 'label': 'Booked',
+ 'data': utilization['booked_seconds'],
+ 'color': '#d9534f'
+ },
+ {
+ 'label': 'Available',
+ 'data': utilization['available_seconds'],
+ 'color': '#5cb85c'
+ },
+ ]
+ return JsonResponse({'data': utilization})
+
+
+class JenkinsUtilizationJSON(View):
+ def get(self, request, *args, **kwargs):
+ resource = get_object_or_404(Resource, id=kwargs['resource_id'])
+ weeks = int(kwargs['weeks'])
+ utilization = resource.slave.get_utilization(timedelta(weeks=weeks))
+ utilization = [
+ {
+ 'label': 'Offline',
+ 'data': utilization['offline'],
+ 'color': '#d9534f'
+ },
+ {
+ 'label': 'Online',
+ 'data': utilization['online'],
+ 'color': '#5cb85c'
+ },
+ {
+ 'label': 'Idle',
+ 'data': utilization['idle'],
+ 'color': '#5bc0de'
+ },
+ ]
+ return JsonResponse({'data': utilization})
diff --git a/pharos-dashboard/src/jenkins/__init__.py b/pharos-dashboard/src/jenkins/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/pharos-dashboard/src/jenkins/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
diff --git a/pharos-dashboard/src/jenkins/adapter.py b/pharos-dashboard/src/jenkins/adapter.py
new file mode 100644
index 0000000..ff0508d
--- /dev/null
+++ b/pharos-dashboard/src/jenkins/adapter.py
@@ -0,0 +1,134 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+import 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:
+ try:
+ response = requests.get(url)
+ json = response.json()
+ cache.set(url, json, 180) # cache result for 180 seconds
+ return json
+ except requests.exceptions.RequestException as e:
+ logger.exception(e)
+ except ValueError as e:
+ logger.exception(e)
+ else:
+ return cache.get(url)
+
+
+def get_all_slaves():
+ url = "https://build.opnfv.org/ci/computer/api/json?tree=computer[displayName,offline,idle]"
+ json = get_json(url)
+ if json is not None:
+ return json['computer'] # return list of dictionaries
+ return []
+
+
+def get_slave(slavename):
+ slaves = get_all_slaves()
+ for slave in slaves:
+ if slave['displayName'] == slavename:
+ return slave
+ return {}
+
+
+def get_ci_slaves():
+ url = "https://build.opnfv.org/ci/label/ci-pod/api/json?tree=nodes[nodeName,offline,idle]"
+ json = get_json(url)
+ if json is not None:
+ return json['nodes']
+ return []
+
+
+def get_all_jobs():
+ url = "https://build.opnfv.org/ci/api/json?tree=jobs[displayName,url,lastBuild[fullDisplayName,building,builtOn,timestamp,result]]"
+ json = get_json(url)
+ if json is not None:
+ return json['jobs'] # return list of dictionaries
+ return []
+
+
+def get_jenkins_job(slavename):
+ jobs = get_all_jobs()
+ max_time = 0
+ last_job = None
+ for job in jobs:
+ if job['lastBuild'] is not None:
+ if job['lastBuild']['builtOn'] == slavename:
+ if job['lastBuild']['building'] is True:
+ return job # return active build
+ if job['lastBuild']['timestamp'] > max_time:
+ last_job = job
+ max_time = job['lastBuild']['timestamp']
+ return last_job
+
+
+def is_ci_slave(slavename):
+ ci_slaves = get_ci_slaves()
+ for ci_slave in ci_slaves:
+ if ci_slave['nodeName'] == slavename:
+ return True
+ return False
+
+
+def is_dev_pod(slavename):
+ if is_ci_slave(slavename):
+ return False
+ 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/pharos-dashboard/src/jenkins/admin.py b/pharos-dashboard/src/jenkins/admin.py
new file mode 100644
index 0000000..c499670
--- /dev/null
+++ b/pharos-dashboard/src/jenkins/admin.py
@@ -0,0 +1,17 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.conf import settings
+from django.contrib import admin
+
+from jenkins.models import JenkinsSlave
+
+if settings.DEBUG:
+ admin.site.register(JenkinsSlave) \ No newline at end of file
diff --git a/pharos-dashboard/src/jenkins/apps.py b/pharos-dashboard/src/jenkins/apps.py
new file mode 100644
index 0000000..41faf60
--- /dev/null
+++ b/pharos-dashboard/src/jenkins/apps.py
@@ -0,0 +1,15 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.apps import AppConfig
+
+
+class JenkinsConfig(AppConfig):
+ name = 'jenkins'
diff --git a/pharos-dashboard/src/jenkins/migrations/__init__.py b/pharos-dashboard/src/jenkins/migrations/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/pharos-dashboard/src/jenkins/migrations/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
diff --git a/pharos-dashboard/src/jenkins/models.py b/pharos-dashboard/src/jenkins/models.py
new file mode 100644
index 0000000..0875bba
--- /dev/null
+++ b/pharos-dashboard/src/jenkins/models.py
@@ -0,0 +1,60 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.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/pharos-dashboard/src/jenkins/tasks.py b/pharos-dashboard/src/jenkins/tasks.py
new file mode 100644
index 0000000..7c03782
--- /dev/null
+++ b/pharos-dashboard/src/jenkins/tasks.py
@@ -0,0 +1,49 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from 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/pharos-dashboard/src/jenkins/tests.py b/pharos-dashboard/src/jenkins/tests.py
new file mode 100644
index 0000000..4f350d2
--- /dev/null
+++ b/pharos-dashboard/src/jenkins/tests.py
@@ -0,0 +1,52 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from unittest import TestCase
+
+import jenkins.adapter as jenkins
+
+
+# Tests that the data we get with the jenkinsadapter contains all the
+# data we need. These test will fail if;
+# - there is no internet connection
+# - the opnfv jenkins url has changed
+# - the jenkins api has changed
+# - jenkins is not set up / there is no data
+class JenkinsAdapterTestCase(TestCase):
+ def test_get_all_slaves(self):
+ slaves = jenkins.get_all_slaves()
+ self.assertTrue(len(slaves) > 0)
+ for slave in slaves:
+ self.assertTrue('displayName' in slave)
+ self.assertTrue('idle' in slave)
+ self.assertTrue('offline' in slave)
+
+ def test_get_ci_slaves(self):
+ slaves = jenkins.get_ci_slaves()
+ self.assertTrue(len(slaves) > 0)
+ for slave in slaves:
+ self.assertTrue('nodeName' in slave)
+
+ def test_get_all_jobs(self):
+ jobs = jenkins.get_all_jobs()
+ lastBuild = False
+ self.assertTrue(len(jobs) > 0)
+ for job in jobs:
+ self.assertTrue('displayName' in job)
+ self.assertTrue('url' in job)
+ self.assertTrue('lastBuild' in job)
+ if job['lastBuild'] is not None:
+ lastBuild = True
+ self.assertTrue('building' in job['lastBuild'])
+ self.assertTrue('fullDisplayName' in job['lastBuild'])
+ self.assertTrue('result' in job['lastBuild'])
+ self.assertTrue('timestamp' in job['lastBuild'])
+ self.assertTrue('builtOn' in job['lastBuild'])
+ self.assertTrue(lastBuild)
diff --git a/pharos-dashboard/src/manage.py b/pharos-dashboard/src/manage.py
new file mode 100644
index 0000000..80c496f
--- /dev/null
+++ b/pharos-dashboard/src/manage.py
@@ -0,0 +1,32 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pharos_dashboard.settings")
+ 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/pharos-dashboard/src/notification/__init__.py b/pharos-dashboard/src/notification/__init__.py
new file mode 100644
index 0000000..37dcbdd
--- /dev/null
+++ b/pharos-dashboard/src/notification/__init__.py
@@ -0,0 +1,11 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+default_app_config = 'notification.apps.NotificationConfig' \ No newline at end of file
diff --git a/pharos-dashboard/src/notification/admin.py b/pharos-dashboard/src/notification/admin.py
new file mode 100644
index 0000000..bcaa1ab
--- /dev/null
+++ b/pharos-dashboard/src/notification/admin.py
@@ -0,0 +1,17 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.conf import settings
+from django.contrib import admin
+
+from notification.models import BookingNotification
+
+if settings.DEBUG:
+ admin.site.register(BookingNotification) \ No newline at end of file
diff --git a/pharos-dashboard/src/notification/apps.py b/pharos-dashboard/src/notification/apps.py
new file mode 100644
index 0000000..2de22c4
--- /dev/null
+++ b/pharos-dashboard/src/notification/apps.py
@@ -0,0 +1,18 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.apps import AppConfig
+
+
+class NotificationConfig(AppConfig):
+ name = 'notification'
+
+ def ready(self):
+ import notification.signals #noqa \ No newline at end of file
diff --git a/pharos-dashboard/src/notification/migrations/__init__.py b/pharos-dashboard/src/notification/migrations/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/pharos-dashboard/src/notification/migrations/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
diff --git a/pharos-dashboard/src/notification/models.py b/pharos-dashboard/src/notification/models.py
new file mode 100644
index 0000000..2d19918
--- /dev/null
+++ b/pharos-dashboard/src/notification/models.py
@@ -0,0 +1,32 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.db import models
+
+class BookingNotification(models.Model):
+ id = models.AutoField(primary_key=True)
+ type = models.CharField(max_length=100)
+ booking = models.ForeignKey('booking.Booking', on_delete=models.CASCADE)
+ submit_time = models.DateTimeField()
+ submitted = models.BooleanField(default=False)
+
+ def get_content(self):
+ return {
+ 'start': self.booking.start.isoformat(),
+ 'end': self.booking.end.isoformat(),
+ 'user': self.booking.user.username,
+ 'purpose': self.booking.purpose
+ }
+
+ def save(self, *args, **kwargs):
+ notifications = self.booking.bookingnotification_set.filter(type=self.type)
+ if notifications.count() > 1:
+ raise ValueError('Doubled Notification')
+ return super(BookingNotification, self).save(*args, **kwargs) \ No newline at end of file
diff --git a/pharos-dashboard/src/notification/signals.py b/pharos-dashboard/src/notification/signals.py
new file mode 100644
index 0000000..936c25b
--- /dev/null
+++ b/pharos-dashboard/src/notification/signals.py
@@ -0,0 +1,25 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+from booking.models import Booking
+from notification.models import BookingNotification
+
+
+@receiver(post_save, sender=Booking)
+def booking_notification_handler(sender, instance, **kwargs):
+ BookingNotification.objects.update_or_create(
+ booking=instance, type='booking_start', defaults={'submit_time': instance.start}
+ )
+ BookingNotification.objects.update_or_create(
+ booking=instance, type='booking_end', defaults={'submit_time': instance.end}
+ ) \ No newline at end of file
diff --git a/pharos-dashboard/src/notification/tasks.py b/pharos-dashboard/src/notification/tasks.py
new file mode 100644
index 0000000..61ab14a
--- /dev/null
+++ b/pharos-dashboard/src/notification/tasks.py
@@ -0,0 +1,33 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from celery import shared_task
+from datetime import timedelta
+
+from django.conf import settings
+from django.utils import timezone
+
+from notification.models import BookingNotification
+from notification_framework.notification import Notification
+
+
+@shared_task
+def send_booking_notifications():
+ messaging = Notification(dashboard_url=settings.RABBITMQ_URL)
+
+ now = timezone.now()
+ notifications = BookingNotification.objects.filter(submitted=False,
+ submit_time__gt=now,
+ submit_time__lt=now + timedelta(minutes=5))
+ for notification in notifications:
+ messaging.send(notification.type, notification.booking.resource.name,
+ notification.get_content())
+ notification.submitted = True
+ notification.save()
diff --git a/pharos-dashboard/src/notification_framework/__init__.py b/pharos-dashboard/src/notification_framework/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/pharos-dashboard/src/notification_framework/__init__.py
@@ -0,0 +1,10 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
diff --git a/pharos-dashboard/src/notification_framework/notification.py b/pharos-dashboard/src/notification_framework/notification.py
new file mode 100644
index 0000000..84fbcff
--- /dev/null
+++ b/pharos-dashboard/src/notification_framework/notification.py
@@ -0,0 +1,114 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+import json
+import re
+
+import pika
+
+
+class Notification(object):
+ """
+ This class can be used by the dashboard and the labs to exchange notifications about booking
+ events and pod status. It utilizes rabbitmq to communicate.
+
+ Notifications are associated to an event and to a topic.
+ Events are:
+ [ 'booking_start', 'booking_stop', 'pod_status' ]
+ The topic is usually a POD name, ie:
+ 'Intel POD 2'
+ """
+
+ def __init__(self, dashboard_url, verbose=False):
+ self.rabbitmq_broker = dashboard_url
+ self.verbose = verbose
+ self._registry = {}
+
+ self.connection = pika.BlockingConnection(pika.ConnectionParameters(
+ host=self.rabbitmq_broker))
+ self.channel = self.connection.channel()
+
+ self.channel.exchange_declare(exchange='notifications', type='topic')
+
+ self.result = self.channel.queue_declare(exclusive=True)
+ self.queue_name = self.result.method.queue
+
+ def register(self, function, event, regex):
+ """
+ Registers a function to be called for the specified event.
+ :param function: the function to register
+ :param event: the event type
+ :param regex: a regex to specify for wich topics the function will be called. Some
+ possible Expressions can be:
+ 'Intel POD 2' : Intel POD 2
+ 'Intel POD .*' : All Intel Pods
+ '.*' : All Topics
+ """
+
+ if event not in self._registry:
+ self._registry[event] = [(function, regex)]
+ else:
+ self._registry[event].append((function, regex))
+
+ def receive(self):
+ """
+ Start receiving notifications. This is a blocking operation, if a notification is received,
+ the registered functions will be called.
+ """
+ if self.verbose:
+ print('Start receiving Notifications. Keys: ', self._registry.keys())
+ self._receive_message(self._registry.keys())
+
+ def send(self, event, topic, content):
+ """
+ Send an event notification.
+ :param event: the event type
+ :param topic: the pod name
+ :param content: a JSON-serializable dictionary
+ """
+ message = {
+ 'event': event,
+ 'topic': topic,
+ 'content': content
+ }
+ self._send_message(message)
+
+ def _send_message(self, event):
+ routing_key = event['type']
+ message = json.dumps(event)
+ self.channel.basic_publish(exchange='notifications',
+ routing_key=routing_key,
+ body=message,
+ properties=pika.BasicProperties(
+ content_type='application/json'
+ ))
+ if self.verbose:
+ print(" [x] Sent %r:%r" % (routing_key, message))
+
+ def _receive_message(self, binding_keys):
+ for key in binding_keys:
+ self.channel.queue_bind(exchange='notifications',
+ queue=self.queue_name,
+ routing_key=key)
+ self.channel.basic_consume(self._message_callback,
+ queue=self.queue_name,
+ no_ack=True)
+ self.channel.start_consuming()
+
+ def _message_callback(self, ch, method, properties, body):
+ if self.verbose:
+ print(" [x] Got %r:%r" % (method.routing_key, body))
+ if method.routing_key not in self._registry:
+ return
+ for func, regex in self._registry[method.routing_key]:
+ message = json.loads(body.decode())
+ match = re.match(regex, message['topic'])
+ if match:
+ func(body)
diff --git a/pharos-dashboard/src/pharos_dashboard/__init__.py b/pharos-dashboard/src/pharos_dashboard/__init__.py
new file mode 100644
index 0000000..f104c4d
--- /dev/null
+++ b/pharos-dashboard/src/pharos_dashboard/__init__.py
@@ -0,0 +1,13 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+# 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/pharos-dashboard/src/pharos_dashboard/celery.py b/pharos-dashboard/src/pharos_dashboard/celery.py
new file mode 100644
index 0000000..f60f243
--- /dev/null
+++ b/pharos-dashboard/src/pharos_dashboard/celery.py
@@ -0,0 +1,30 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+import 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/pharos-dashboard/src/pharos_dashboard/settings.py b/pharos-dashboard/src/pharos_dashboard/settings.py
new file mode 100644
index 0000000..2c4e8cc
--- /dev/null
+++ b/pharos-dashboard/src/pharos_dashboard/settings.py
@@ -0,0 +1,176 @@
+import os
+from datetime import timedelta
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = os.environ['DEBUG']
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'dashboard',
+ 'booking',
+ 'account',
+ 'jenkins',
+ 'notification',
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'django.contrib.humanize',
+ 'bootstrap3',
+ 'crispy_forms',
+ 'rest_framework',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ 'account.middleware.TimezoneMiddleware',
+]
+
+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'
+
+# Password validation
+# https://docs.djangoproject.com/en/1.10/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',
+ },
+]
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.10/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.10/howto/static-files/
+MEDIA_URL = '/media/'
+STATIC_URL = '/static/'
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.10/howto/static-files/
+STATICFILES_DIRS = [
+ os.path.join(BASE_DIR, "static"),
+]
+
+LOGIN_REDIRECT_URL = '/'
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = os.environ['SECRET_KEY']
+
+BOOTSTRAP3 = {
+ 'set_placeholder': False,
+}
+
+ALLOWED_HOSTS = ['*']
+
+# Database
+# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.postgresql',
+ 'NAME': os.environ['DB_NAME'],
+ 'USER': os.environ['DB_USER'],
+ 'PASSWORD': os.environ['DB_PASS'],
+ 'HOST': os.environ['DB_SERVICE'],
+ 'PORT': os.environ['DB_PORT']
+ }
+}
+
+
+# Rest API Settings
+REST_FRAMEWORK = {
+ 'DEFAULT_PERMISSION_CLASSES': [
+ 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
+ ],
+ 'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',)
+}
+
+MEDIA_ROOT = '/media'
+STATIC_ROOT = '/static'
+
+# Jira Settings
+CREATE_JIRA_TICKET = False
+
+JIRA_URL = os.environ['JIRA_URL']
+
+JIRA_USER_NAME = os.environ['JIRA_USER_NAME']
+JIRA_USER_PASSWORD = os.environ['JIRA_USER_PASSWORD']
+
+OAUTH_CONSUMER_KEY = os.environ['OAUTH_CONSUMER_KEY']
+OAUTH_CONSUMER_SECRET = os.environ['OAUTH_CONSUMER_SECRET']
+
+OAUTH_REQUEST_TOKEN_URL = JIRA_URL + '/plugins/servlet/oauth/request-token'
+OAUTH_ACCESS_TOKEN_URL = JIRA_URL + '/plugins/servlet/oauth/access-token'
+OAUTH_AUTHORIZE_URL = JIRA_URL + '/plugins/servlet/oauth/authorize'
+
+OAUTH_CALLBACK_URL = JIRA_URL + '/accounts/authenticated'
+
+# Celery Settings
+CELERY_TIMEZONE = 'UTC'
+
+RABBITMQ_URL = 'rabbitmq'
+BROKER_URL = 'amqp://guest:guest@rabbitmq:5672//'
+
+CELERYBEAT_SCHEDULE = {
+ 'sync-jenkins': {
+ 'task': 'jenkins.tasks.sync_jenkins',
+ 'schedule': timedelta(minutes=5)
+ },
+ 'send-booking-notifications': {
+ 'task': 'notification.tasks.send_booking_notifications',
+ 'schedule': timedelta(minutes=5)
+ },
+ 'clean-database': {
+ 'task': 'dashboard.tasks.database_cleanup',
+ 'schedule': timedelta(hours=24)
+ },
+} \ No newline at end of file
diff --git a/pharos-dashboard/src/pharos_dashboard/urls.py b/pharos-dashboard/src/pharos_dashboard/urls.py
new file mode 100644
index 0000000..adcb5b8
--- /dev/null
+++ b/pharos-dashboard/src/pharos_dashboard/urls.py
@@ -0,0 +1,44 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+"""pharos_dashboard URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/1.10/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.conf.urls import url, include
+ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
+"""
+from django.conf import settings
+from django.conf.urls import url, include
+from django.conf.urls.static import static
+from django.contrib import admin
+
+
+urlpatterns = [
+ url(r'^', include('dashboard.urls', namespace='dashboard')),
+ url(r'^booking/', include('booking.urls', namespace='booking')),
+ url(r'^accounts/', include('account.urls', namespace='account')),
+
+ url(r'^admin/', admin.site.urls),
+ url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
+
+ url(r'^api/', include('api.urls'))
+]
+
+if settings.DEBUG is True:
+ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file
diff --git a/pharos-dashboard/src/pharos_dashboard/wsgi.py b/pharos-dashboard/src/pharos_dashboard/wsgi.py
new file mode 100644
index 0000000..3d43361
--- /dev/null
+++ b/pharos-dashboard/src/pharos_dashboard/wsgi.py
@@ -0,0 +1,26 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+"""
+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.10/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pharos_dashboard.settings")
+
+application = get_wsgi_application()
diff --git a/pharos-dashboard/src/static/bower.json b/pharos-dashboard/src/static/bower.json
new file mode 100644
index 0000000..f473747
--- /dev/null
+++ b/pharos-dashboard/src/static/bower.json
@@ -0,0 +1,24 @@
+{
+ "name": "pharos-dashboard-dependencies",
+ "authors": [
+ "maxbr <maxbr@mi.fu-berlin.de>"
+ ],
+ "description": "This package contains all the Js/CSS dependencies needed to run the Pharos Dashboard.",
+ "main": "",
+ "license": "Apache2",
+ "homepage": "",
+ "private": true,
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "test",
+ "tests"
+ ],
+ "dependencies": {
+ "eonasdan-bootstrap-datetimepicker": "^4.17.37",
+ "fullcalendar": "^2.9.0",
+ "jquery-migrate": "^3.0.0",
+ "startbootstrap-sb-admin-2-blackrockdigital": "^3.3.7"
+ }
+}
diff --git a/pharos-dashboard/src/static/css/theme.css b/pharos-dashboard/src/static/css/theme.css
new file mode 100644
index 0000000..bd15637
--- /dev/null
+++ b/pharos-dashboard/src/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/pharos-dashboard/src/static/js/booking-calendar.js b/pharos-dashboard/src/static/js/booking-calendar.js
new file mode 100644
index 0000000..9cb0f32
--- /dev/null
+++ b/pharos-dashboard/src/static/js/booking-calendar.js
@@ -0,0 +1,46 @@
+/*****************************************************************************
+* Copyright (c) 2016 Max Breitenfeldt and others.
+*
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Apache License, Version 2.0
+* which accompanies this distribution, and is available at
+* http://www.apache.org/licenses/LICENSE-2.0
+*****************************************************************************/
+
+
+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);
+});
diff --git a/pharos-dashboard/src/static/js/dataTables-sort.js b/pharos-dashboard/src/static/js/dataTables-sort.js
new file mode 100644
index 0000000..3072d2f
--- /dev/null
+++ b/pharos-dashboard/src/static/js/dataTables-sort.js
@@ -0,0 +1,36 @@
+/*****************************************************************************
+* Copyright (c) 2016 Max Breitenfeldt and others.
+*
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Apache License, Version 2.0
+* which accompanies this distribution, and is available at
+* http://www.apache.org/licenses/LICENSE-2.0
+*****************************************************************************/
+
+
+/**
+ * This is a sort function for dataTables to sort tables by the status column.
+ * The order should be: online < online/idle < offline
+ */
+jQuery.extend(jQuery.fn.dataTableExt.oSort, {
+ "status-pre": function (a) {
+ switch (a) {
+ case 'online':
+ return 1;
+ case 'online / idle':
+ return 2;
+ case 'offline':
+ return 3;
+ default:
+ return a;
+ }
+ },
+
+ "status-asc": function (a, b) {
+ return ((a < b) ? -1 : ((a > b) ? 1 : 0));
+ },
+
+ "status-desc": function (a, b) {
+ return ((a < b) ? 1 : ((a > b) ? -1 : 0));
+ }
+}); \ No newline at end of file
diff --git a/pharos-dashboard/src/static/js/datetimepicker-options.js b/pharos-dashboard/src/static/js/datetimepicker-options.js
new file mode 100644
index 0000000..d43f5fb
--- /dev/null
+++ b/pharos-dashboard/src/static/js/datetimepicker-options.js
@@ -0,0 +1,13 @@
+/*****************************************************************************
+* Copyright (c) 2016 Max Breitenfeldt and others.
+*
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Apache License, Version 2.0
+* which accompanies this distribution, and is available at
+* http://www.apache.org/licenses/LICENSE-2.0
+*****************************************************************************/
+
+
+var timepickerOptions = {
+ format: 'MM/DD/YYYY HH:00'
+}; \ No newline at end of file
diff --git a/pharos-dashboard/src/static/js/flot-pie-chart.js b/pharos-dashboard/src/static/js/flot-pie-chart.js
new file mode 100644
index 0000000..3b80b2a
--- /dev/null
+++ b/pharos-dashboard/src/static/js/flot-pie-chart.js
@@ -0,0 +1,30 @@
+/*****************************************************************************
+* Copyright (c) 2016 Max Breitenfeldt and others.
+*
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Apache License, Version 2.0
+* which accompanies this distribution, and is available at
+* http://www.apache.org/licenses/LICENSE-2.0
+*****************************************************************************/
+
+
+function loadChartData(chart_id, url) {
+ $.ajax({
+ url: url,
+ type: 'get',
+ success: function (data) {
+ var data = data['data'];
+ var plotObj = $.plot($("#" + chart_id), data, {
+ series: {
+ pie: {
+ show: true
+ }
+ }
+ });
+ },
+ failure: function (data) {
+ alert('Error loading data');
+ }
+ });
+
+} \ No newline at end of file
diff --git a/pharos-dashboard/src/static/js/fullcalendar-options.js b/pharos-dashboard/src/static/js/fullcalendar-options.js
new file mode 100644
index 0000000..22a1b95
--- /dev/null
+++ b/pharos-dashboard/src/static/js/fullcalendar-options.js
@@ -0,0 +1,101 @@
+/*****************************************************************************
+* Copyright (c) 2016 Max Breitenfeldt and others.
+*
+* All rights reserved. This program and the accompanying materials
+* are made available under the terms of the Apache License, Version 2.0
+* which accompanies this distribution, and is available at
+* http://www.apache.org/licenses/LICENSE-2.0
+*****************************************************************************/
+
+
+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/pharos-dashboard/src/templates/account/user_list.html b/pharos-dashboard/src/templates/account/user_list.html
new file mode 100644
index 0000000..c2b8193
--- /dev/null
+++ b/pharos-dashboard/src/templates/account/user_list.html
@@ -0,0 +1,46 @@
+{% extends "dashboard/table.html" %}
+{% load staticfiles %}
+
+{% block table %}
+ <thead>
+ <tr>
+ <th>Username</th>
+ <th>Company</th>
+ <th>SSH Key</th>
+ <th>GPG Key</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for user in users %}
+ <tr>
+ <td>
+ {{ user.username }}
+ </td>
+ <td>
+ {{ user.userprofile.company }}
+ </td>
+ <td>
+ {% if user.userprofile.ssh_public_key %}
+ <a href={{ user.userprofile.ssh_public_key.url }}>SSH</a>
+ {% endif %}
+ </td>
+ <td>
+ {% if user.userprofile.pgp_public_key %}
+ <a href={{ user.userprofile.pgp_public_key.url }}>GPG</a>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+{% endblock table %}
+
+
+{% block tablejs %}
+ <script type="text/javascript">
+ $(document).ready(function () {
+ $('#table').DataTable({
+ "order": [[0, "asc"]]
+ });
+ });
+ </script>
+{% endblock tablejs %}
diff --git a/pharos-dashboard/src/templates/account/userprofile_update_form.html b/pharos-dashboard/src/templates/account/userprofile_update_form.html
new file mode 100644
index 0000000..542ea81
--- /dev/null
+++ b/pharos-dashboard/src/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/pharos-dashboard/src/templates/base.html b/pharos-dashboard/src/templates/base.html
new file mode 100644
index 0000000..5bb5547
--- /dev/null
+++ b/pharos-dashboard/src/templates/base.html
@@ -0,0 +1,104 @@
+{% extends "layout.html" %}
+{% load bootstrap3 %}
+
+{% block basecontent %}
+ <div id="wrapper">
+ <!-- Navigation -->
+ <nav class="navbar navbar-default navbar-static-top" role="navigation"
+ style="margin-bottom: 0">
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle" data-toggle="collapse"
+ data-target=".navbar-collapse">
+ <span class="sr-only">Toggle navigation</span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </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:index' %}>Pharos Dashboard</a>
+ </div>
+ <!-- /.navbar-header -->
+
+ <ul class="nav navbar-top-links navbar-right">
+ <li class="dropdown">
+ <a class="dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="fa fa-user fa-fw"></i> <i class="fa fa-caret-down"></i>
+ </a>
+ <ul class="dropdown-menu dropdown-user">
+ {% if user.is_authenticated %}
+ <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 'account:logout' %}?next={{ request.path }}"><i
+ class="fa fa-sign-out fa-fw"></i>
+ Logout</a>
+ </li>
+ {% else %}
+ <li><a href="{% url 'account:login' %}"><i
+ class="fa fa-sign-in fa-fw"></i>
+ Login with Jira</a>
+ <li>
+ {% endif %}
+ </ul>
+ <!-- /.dropdown-user -->
+ </li>
+ <!-- /.dropdown -->
+ </ul>
+ <!-- /.navbar-top-links -->
+
+ <div class="navbar-default sidebar" role="navigation">
+ <div class="sidebar-nav navbar-collapse">
+ <ul class="nav" id="side-menu">
+ <li>
+ <a href="{% url 'dashboard:ci_pods' %}"><i
+ class="fa fa-fw"></i>CI-Pods</a>
+ </li>
+ <li>
+ <a href="{% url 'dashboard:dev_pods' %}"><i
+ class="fa fa-fw"></i>Development
+ Pods</a>
+ </li>
+ <li>
+ <a href="{% url 'dashboard:jenkins_slaves' %}"><i
+ class="fa fa-fw"></i>Jenkins
+ Slaves</a>
+ </li>
+ <li>
+ <a href="{% url 'account:users' %}"><i
+ class="fa fa-fw"></i>Users
+ </a>
+ </li>
+ <li>
+ <a href="{% url 'api-root' %}"><i
+ class="fa fa-fw"></i>API
+ </a>
+ </li>
+ </ul>
+ </div>
+ <!-- /.sidebar-collapse -->
+ </div>
+ <!-- /.navbar-static-side -->
+ </nav>
+
+ <!-- Page Content -->
+ <div id="page-wrapper">
+ <div class="row">
+ <div class="col-lg-12">
+ <h1 class="page-header">{{ title }}</h1>
+ </div>
+ <!-- /.col-lg-12 -->
+ </div>
+
+ {% bootstrap_messages %}
+
+ {% block content %}
+
+ {% endblock content %}
+ </div>
+ <!-- /#page-wrapper -->
+ </div>
+ <!-- /#wrapper -->
+{% endblock basecontent %} \ No newline at end of file
diff --git a/pharos-dashboard/src/templates/booking/booking_calendar.html b/pharos-dashboard/src/templates/booking/booking_calendar.html
new file mode 100644
index 0000000..de3e3b3
--- /dev/null
+++ b/pharos-dashboard/src/templates/booking/booking_calendar.html
@@ -0,0 +1,94 @@
+{% extends "dashboard/table.html" %}
+{% load staticfiles %}
+
+{% load bootstrap3 %}
+
+{% block extrahead %}
+ <link href="{% static "bower_components/fullcalendar/dist/fullcalendar.css" %}"
+ rel='stylesheet'/>
+ <link href="{% static "bower_components/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css" %}"
+ rel='stylesheet'/>
+{% endblock extrahead %}
+
+{% block content %}
+ <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>
+ </div>
+ <!-- /.panel-body -->
+ </div>
+ <!-- /.panel -->
+ </div>
+ </div>
+
+ <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" action="" class="form" id="bookingform">
+ {% csrf_token %}
+
+ <div class='input-group' id='starttimepicker'>
+ {% bootstrap_field form.start addon_after='<span class="glyphicon glyphicon-calendar"></span>' %}
+ </div>
+ <div class='input-group' id='endtimepicker'>
+ {% bootstrap_field form.end addon_after='<span class="glyphicon glyphicon-calendar"></span>' %}
+ </div>
+ {% bootstrap_field form.purpose %}
+
+ {% buttons %}
+ <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 = "{% 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>
+ <script src={% static "bower_components/fullcalendar/dist/fullcalendar.js" %}></script>
+ <script type="text/javascript"
+ 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/booking-calendar.js" %}></script>
+{% endblock extrajs %} \ No newline at end of file
diff --git a/pharos-dashboard/src/templates/booking/booking_detail.html b/pharos-dashboard/src/templates/booking/booking_detail.html
new file mode 100644
index 0000000..d3f4753
--- /dev/null
+++ b/pharos-dashboard/src/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/pharos-dashboard/src/templates/booking/booking_table.html b/pharos-dashboard/src/templates/booking/booking_table.html
new file mode 100644
index 0000000..216eaf5
--- /dev/null
+++ b/pharos-dashboard/src/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>
+ <td>
+ {{ booking.user.username }}
+ </td>
+ <td>
+ {{ booking.purpose }}
+ </td>
+ <td>
+ {{ booking.start }}
+ </td>
+ <td>
+ {{ booking.end }}
+ </td>
+ <td><a target='_blank'
+ href={{ booking.get_jira_issue | jira_issue_url }}>{{ booking.get_jira_issue }}</a>
+ </td>
+ </tr>
+{% endfor %}
+</tbody> \ No newline at end of file
diff --git a/pharos-dashboard/src/templates/dashboard/ci_pods.html b/pharos-dashboard/src/templates/dashboard/ci_pods.html
new file mode 100644
index 0000000..7ef62a4
--- /dev/null
+++ b/pharos-dashboard/src/templates/dashboard/ci_pods.html
@@ -0,0 +1,60 @@
+{% extends "dashboard/table.html" %}
+{% load staticfiles %}
+{% load jenkins_filters %}
+
+{% block table %}
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Slave Name</th>
+ <th>Status</th>
+ <th>Installer</th>
+ <th>Scenario</th>
+ <th>Branch</th>
+ <th>Job</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for pod in ci_pods %}
+ <tr>
+ <td>
+ <a target='_blank' href={{ pod.url }}>{{ pod.name }}</a>
+ </td>
+ <td>
+ <a target='_blank' href={{ pod.slave.url }}>{{ pod.slave.name }}</a>
+ </td>
+ <td style="background-color:{{ pod.slave.status | jenkins_status_color }}">
+ {{ pod.slave.status }}
+ </td>
+ <td {{ pod.slave.last_job_result | jenkins_job_blink }}>
+ {{ pod.slave.last_job_installer }}
+ </td>
+ <td {{ pod.slave.last_job_result | jenkins_job_blink }}>
+ {{ pod.slave.last_job_scenario }}
+ </td>
+ <td {{ pod.slave.last_job_result | jenkins_job_blink }}>
+ {{ pod.slave.last_job_branch }}
+ </td>
+ <td><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>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+{% endblock table %}
+
+
+{% block tablejs %}
+ <script type="text/javascript">
+ $(document).ready(function () {
+ $('#table').DataTable({
+ columnDefs: [
+ {type: 'status', targets: 2}
+ ],
+ "order": [[2, "asc"]]
+ });
+ });
+ </script>
+{% endblock tablejs %} \ No newline at end of file
diff --git a/pharos-dashboard/src/templates/dashboard/dev_pods.html b/pharos-dashboard/src/templates/dashboard/dev_pods.html
new file mode 100644
index 0000000..2b4b017
--- /dev/null
+++ b/pharos-dashboard/src/templates/dashboard/dev_pods.html
@@ -0,0 +1,69 @@
+{% extends "dashboard/table.html" %}
+{% load staticfiles %}
+{% load jenkins_filters %}
+
+{% block table %}
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Slave Name</th>
+ <th>Booked by</th>
+ <th>Booked until</th>
+ <th>Purpose</th>
+ <th>Utilization</th>
+ <th>Status</th>
+ <th></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for pod, booking, utilization in dev_pods %}
+ <tr>
+ <td>
+ <a href={% url 'dashboard:resource' resource_id=pod.id %}>{{ pod.name }}</a>
+ </td>
+ <td>
+ <a target='_blank' href={{ pod.slave.url }}>{{ pod.slave.name }}</a>
+ </td>
+ <td>
+ {{ booking.user.username }}
+ </td>
+ <td>
+ {{ booking.end }}
+ </td>
+ <td>
+ {{ booking.purpose }}
+ </td>
+ <td>
+ {{ utilization }}
+ </td>
+ <td style="background-color:{{ pod.slave.status | jenkins_status_color }}">
+ {{ pod.slave.status }}
+ </td>
+ <td>
+ <a href="{% url 'booking:create' resource_id=pod.id %}" class="btn btn-primary">
+ Book
+ </a>
+ </td>
+ <td>
+ <a href="{% url 'dashboard:resource' resource_id=pod.id %}" class="btn btn-primary">
+ Info
+ </a>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+{% endblock table %}
+
+{% block tablejs %}
+ <script type="text/javascript">
+ $(document).ready(function () {
+ $('#table').DataTable({
+ columnDefs: [
+ {type: 'status', targets: 6}
+ ],
+ "order": [[6, "asc"]]
+ });
+ });
+ </script>
+{% endblock tablejs %} \ No newline at end of file
diff --git a/pharos-dashboard/src/templates/dashboard/jenkins_slaves.html b/pharos-dashboard/src/templates/dashboard/jenkins_slaves.html
new file mode 100644
index 0000000..aa74507
--- /dev/null
+++ b/pharos-dashboard/src/templates/dashboard/jenkins_slaves.html
@@ -0,0 +1,45 @@
+{% extends "dashboard/table.html" %}
+{% load staticfiles %}
+
+{% load jenkins_filters %}
+
+{% block table %}
+ <thead>
+ <tr>
+ <th>Slave name</th>
+ <th>Status</th>
+ <th>Job</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for slave in slaves %}
+ <tr>
+ <td><a target='_blank'
+ href={{ slave.url }}>{{ slave.name }}</a>
+ </td>
+ <td style="background-color:{{ slave.status | jenkins_status_color }}">
+ {{ slave.status }}
+ </td>
+ <td><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>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+{% endblock table %}
+
+
+{% block tablejs %}
+ <script type="text/javascript">
+ $(document).ready(function () {
+ $('#table').DataTable({
+ columnDefs: [
+ {type: 'status', targets: 1}
+ ],
+ "order": [[1, "asc"]]
+ });
+ });
+ </script>
+{% endblock tablejs %} \ No newline at end of file
diff --git a/pharos-dashboard/src/templates/dashboard/resource.html b/pharos-dashboard/src/templates/dashboard/resource.html
new file mode 100644
index 0000000..c9e5735
--- /dev/null
+++ b/pharos-dashboard/src/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 src="{% static "js/flot-pie-chart.js" %}"></script>
+
+ <script type="text/javascript">
+ $(document).ready(function () {
+ $('#{{ resource.id }}_server_table').DataTable({});
+ $('#{{ resource.id }}_bookings_table').DataTable({});
+ $('#{{ resource.id }}_vpn_user_table').DataTable({});
+
+ var chart_id = "{{ resource.id }}_booking_utilization";
+ var utilization_url = "{% url 'dashboard:booking_utilization' resource_id=resource.id weeks=4 %}";
+ loadChartData(chart_id, utilization_url);
+
+ var chart_id = "{{ resource.id }}_jenkins_utilization";
+ var utilization_url = "{% url 'dashboard:jenkins_utilization' resource_id=resource.id weeks=1 %}";
+ loadChartData(chart_id, utilization_url);
+ });
+ </script>
+{% endblock extrajs %} \ No newline at end of file
diff --git a/pharos-dashboard/src/templates/dashboard/resource_all.html b/pharos-dashboard/src/templates/dashboard/resource_all.html
new file mode 100644
index 0000000..a770d4e
--- /dev/null
+++ b/pharos-dashboard/src/templates/dashboard/resource_all.html
@@ -0,0 +1,73 @@
+{% 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 src="{% static "js/flot-pie-chart.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({});
+ $('#{{ resource.id }}_vpn_user_table').DataTable({});
+
+ var chart_id = "{{ resource.id }}_booking_utilization";
+ var utilization_url = "{% url 'dashboard:booking_utilization' resource_id=resource.id weeks=4 %}";
+ loadChartData(chart_id, utilization_url);
+
+ var chart_id = "{{ resource.id }}_jenkins_utilization";
+ var utilization_url = "{% url 'dashboard:jenkins_utilization' resource_id=resource.id weeks=1 %}";
+ loadChartData(chart_id, utilization_url);
+ {% endfor %}
+ });
+ </script>
+{% endblock extrajs %} \ No newline at end of file
diff --git a/pharos-dashboard/src/templates/dashboard/resource_detail.html b/pharos-dashboard/src/templates/dashboard/resource_detail.html
new file mode 100644
index 0000000..657d565
--- /dev/null
+++ b/pharos-dashboard/src/templates/dashboard/resource_detail.html
@@ -0,0 +1,184 @@
+{% load jenkins_filters %}
+
+<div class="row">
+ <div class="col-lg-3">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ Jenkins Utilization
+ <div class="pull-right">
+ <div class="form-group">
+ <select onchange="loadChartData('{{ resource.id }}_jenkins_utilization', this.value);">
+ <option value="{% url 'dashboard:jenkins_utilization' resource_id=resource.id weeks=1 %}">
+ Last Week
+ </option>
+ <option value="{% url 'dashboard:jenkins_utilization' resource_id=resource.id weeks=4 %}">
+ Last Month
+ </option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div class="panel-body">
+ <div class="flot-chart">
+ <div class="flot-chart-content"
+ id="{{ resource.id }}_jenkins_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-3">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ Booking Utilization
+ <div class="pull-right">
+ <div class="form-group">
+ <select onchange="loadChartData('{{ resource.id }}_booking_utilization', this.value);">
+ <option value="{% url 'dashboard:booking_utilization' resource_id=resource.id weeks=-4 %}">
+ Last Month
+ </option>
+ <option value="{% url 'dashboard:booking_utilization' resource_id=resource.id weeks=-1 %}">
+ Last Week
+ </option>
+ <option value="{% url 'dashboard:booking_utilization' resource_id=resource.id weeks=1 %}">
+ Next Week
+ </option>
+ <option selected="selected"
+ value="{% url 'dashboard:booking_utilization' resource_id=resource.id weeks=4 %}">
+ Next Month
+ </option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div class="panel-body">
+ <div class="flot-chart">
+ <div class="flot-chart-content"
+ id="{{ resource.id }}_booking_utilization"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-9">
+ <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>
+<div class="row">
+ <div class="col-lg-3">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ Contact
+ </div>
+ <div class="panel-body">
+ <p>
+ <b>Lab Owner: </b>
+ {{ resource.owner.username }}
+ </p>
+ <p>
+ <b>Email: </b>
+ </p>
+ <p>
+ <a href="{% url 'booking:create' resource_id=resource.id %}" class="btn
+ btn-primary">
+ Booking
+ </a>
+ <a href="{{ resource.url }}" class="btn
+ btn-primary">
+ OPNFV Wiki
+ </a>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-3">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ Jenkins Status
+ </div>
+ <div class="panel-body">
+ <p>
+ <b>Slave Name: </b>
+ <a target='_blank'
+ href={{ resource.slave.url }}>{{ resource.slave.name }}</a>
+ </p>
+ <p>
+ <b>Status: </b>
+ {{ resource.slave.status }}
+ </p>
+ <p>
+ <b>Last Job: </b>
+ <a href="{{ resource.slave.last_job_url }}">
+ {{ resource.slave.last_job_name }}
+ </a>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ VPN Users
+ </div>
+ <div class="panel-body">
+ <div class="dataTables_wrapper">
+ <table class="table table-striped table-bordered table-hover"
+ id="{{ resource.id }}_vpn_user_table" cellspacing="0"
+ width="100%">
+ <thead>
+ <tr>
+ <th>User</th>
+ <th>Email</th>
+ <th>Company</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for user in resource.vpn_users.all %}
+ <tr>
+ <td>
+ {{ user.username }}
+ </td>
+ <td>
+ {{ user.email }}
+ </td>
+ <td>
+ {{ user.userprofile.company }}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ </tbody>
+ </div>
+ </div>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/pharos-dashboard/src/templates/dashboard/server_table.html b/pharos-dashboard/src/templates/dashboard/server_table.html
new file mode 100644
index 0000000..f01bd60
--- /dev/null
+++ b/pharos-dashboard/src/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>
+ <td>
+ {{ server.name }}
+ </td>
+ <td>
+ {{ server.model }}
+ </td>
+ <td>
+ {{ server.cpu }}
+ </td>
+ <td>
+ {{ server.ram }}
+ </td>
+ <td>
+ {{ server.storage }}
+ </td>
+ </tr>
+{% endfor %}
+</tbody> \ No newline at end of file
diff --git a/pharos-dashboard/src/templates/dashboard/table.html b/pharos-dashboard/src/templates/dashboard/table.html
new file mode 100644
index 0000000..addd5c1
--- /dev/null
+++ b/pharos-dashboard/src/templates/dashboard/table.html
@@ -0,0 +1,50 @@
+{% extends "base.html" %}
+{% load staticfiles %}
+
+{% block extrahead %}
+ <!-- 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 %}
+ <div class="row">
+ <div class="col-lg-12">
+ <div class="panel panel-default">
+ <div class="panel-body">
+ <div class="dataTables_wrapper">
+ <table class="table table-striped table-bordered table-hover" id="table" cellspacing="0"
+ width="100%">
+
+ {% block table %}
+ {% endblock table %}
+
+ </table>
+ </div>
+ <!-- /.table-responsive -->
+ </div>
+ <!-- /.panel-body -->
+ </div>
+ <!-- /.panel -->
+ </div>
+ <!-- /.col-lg-12 -->
+ </div>
+{% 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>
+
+ <script src={% static "js/dataTables-sort.js" %}></script>
+
+ {% block tablejs %}
+ {% endblock tablejs %}
+{% endblock extrajs %} \ No newline at end of file
diff --git a/pharos-dashboard/src/templates/layout.html b/pharos-dashboard/src/templates/layout.html
new file mode 100644
index 0000000..64fed4a
--- /dev/null
+++ b/pharos-dashboard/src/templates/layout.html
@@ -0,0 +1,73 @@
+{% load staticfiles %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="description" content="">
+ <meta name="author" content="">
+
+ <title>OPNFV Pharos {{ title }}</title>
+
+ <!-- Bootstrap Core CSS -->
+ <link href="{% static "bower_components/bootstrap/dist/css/bootstrap.min.css" %}"
+ rel="stylesheet">
+
+ <!-- MetisMenu CSS -->
+ <link href="{% static "bower_components/metisMenu/dist/metisMenu.min.css" %}" rel="stylesheet">
+
+ <!-- Custom CSS -->
+ <link href="{% static "bower_components/startbootstrap-sb-admin-2-blackrockdigital/dist/css/sb-admin-2.css" %}"
+ rel="stylesheet">
+ <link href="{% static "css/theme.css" %}" rel="stylesheet">
+
+ <!-- Custom Fonts -->
+ <link href="{% static "bower_components/font-awesome/css/font-awesome.min.css" %}"
+ rel="stylesheet" type="text/css">
+
+ <!-- Favicon -->
+ <link rel="shortcut icon" href="{% static 'favicon.ico' %}">
+
+ {% block extrahead %}
+ {% endblock extrahead %}
+
+ <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
+ <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
+ <!--[if lt IE 9]>
+ <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
+ <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
+ <![endif]-->
+
+</head>
+
+{% block extrastyle %}
+{% endblock extrastyle %}
+
+<body>
+{% block basecontent %}
+{% endblock basecontent %}
+
+
+<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 -->
+<script src="{% static "bower_components/bootstrap/dist/js/bootstrap.min.js" %}"></script>
+
+<!-- Metis Menu Plugin JavaScript -->
+<script src="{% static "bower_components/metisMenu/dist/metisMenu.min.js" %}"></script>
+
+<!-- Custom Theme JavaScript -->
+<script src="{% static "bower_components/startbootstrap-sb-admin-2-blackrockdigital/dist/js/sb-admin-2.js" %}"></script>
+
+{% block extrajs %}
+{% endblock extrajs %}
+</body>
+</html>
diff --git a/pharos-dashboard/src/templates/rest_framework/api.html b/pharos-dashboard/src/templates/rest_framework/api.html
new file mode 100644
index 0000000..9c6c4f7
--- /dev/null
+++ b/pharos-dashboard/src/templates/rest_framework/api.html
@@ -0,0 +1,9 @@
+{% extends "rest_framework/base.html" %}
+
+{% block title %}Pharos Dashboard API{% endblock %}
+
+{% block branding %}
+ <a class='navbar-brand' rel="nofollow" href=#>
+ Pharos Dashboard API
+ </a>
+{% endblock %} \ No newline at end of file