summaryrefslogtreecommitdiffstats
path: root/dashboard/src/dashboard
diff options
context:
space:
mode:
authorTrevor Bramwell <tbramwell@linuxfoundation.org>2017-09-22 12:23:36 -0700
committerTrevor Bramwell <tbramwell@linuxfoundation.org>2017-09-22 12:23:36 -0700
commit4b269fba0ca273dfa3acf44c9f5490f01e0c3d87 (patch)
tree909b5262b46306eb78327e535f2e14120e45116a /dashboard/src/dashboard
parentd46ab54583a6c20bfa5bea581f512474f488e788 (diff)
Rename pharos-dashboard and pharos-validator
As subdirectories of the pharos-tools repo, there is little need to keep the pharos prefix. Change-Id: Ica3d79411f409df638647300036c0664183c2725 Signed-off-by: Trevor Bramwell <tbramwell@linuxfoundation.org>
Diffstat (limited to 'dashboard/src/dashboard')
-rw-r--r--dashboard/src/dashboard/__init__.py10
-rw-r--r--dashboard/src/dashboard/admin.py20
-rw-r--r--dashboard/src/dashboard/apps.py15
-rw-r--r--dashboard/src/dashboard/fixtures/dashboard.json164
-rw-r--r--dashboard/src/dashboard/migrations/0001_initial.py64
-rw-r--r--dashboard/src/dashboard/migrations/0002_auto_20170505_0815.py42
-rw-r--r--dashboard/src/dashboard/migrations/__init__.py10
-rw-r--r--dashboard/src/dashboard/models.py95
-rw-r--r--dashboard/src/dashboard/tasks.py24
-rw-r--r--dashboard/src/dashboard/templatetags/__init__.py10
-rw-r--r--dashboard/src/dashboard/templatetags/jenkins_filters.py38
-rw-r--r--dashboard/src/dashboard/templatetags/jira_filters.py17
-rw-r--r--dashboard/src/dashboard/tests/__init__.py10
-rw-r--r--dashboard/src/dashboard/tests/test_models.py69
-rw-r--r--dashboard/src/dashboard/tests/test_views.py75
-rw-r--r--dashboard/src/dashboard/urls.py41
-rw-r--r--dashboard/src/dashboard/views.py141
17 files changed, 845 insertions, 0 deletions
diff --git a/dashboard/src/dashboard/__init__.py b/dashboard/src/dashboard/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/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/dashboard/src/dashboard/admin.py b/dashboard/src/dashboard/admin.py
new file mode 100644
index 0000000..0bfdef8
--- /dev/null
+++ b/dashboard/src/dashboard/admin.py
@@ -0,0 +1,20 @@
+##############################################################################
+# 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.site_header = "Pharos Dashboard Administration"
+admin.site.site_title = "Pharos Dashboard"
+
+admin.site.register(Resource)
+admin.site.register(Server)
+admin.site.register(ResourceStatus)
diff --git a/dashboard/src/dashboard/apps.py b/dashboard/src/dashboard/apps.py
new file mode 100644
index 0000000..e0c4f44
--- /dev/null
+++ b/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/dashboard/src/dashboard/fixtures/dashboard.json b/dashboard/src/dashboard/fixtures/dashboard.json
new file mode 100644
index 0000000..f0ac3b2
--- /dev/null
+++ b/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/dashboard/src/dashboard/migrations/0001_initial.py b/dashboard/src/dashboard/migrations/0001_initial.py
new file mode 100644
index 0000000..aaf3945
--- /dev/null
+++ b/dashboard/src/dashboard/migrations/0001_initial.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10 on 2016-11-03 13:33
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('jenkins', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Resource',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('name', models.CharField(max_length=100, unique=True)),
+ ('description', models.CharField(blank=True, max_length=300, null=True)),
+ ('url', models.CharField(blank=True, max_length=100, null=True)),
+ ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_lab_owner', to=settings.AUTH_USER_MODEL)),
+ ('slave', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='jenkins.JenkinsSlave')),
+ ('vpn_users', models.ManyToManyField(blank=True, related_name='user_vpn_users', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'db_table': 'resource',
+ },
+ ),
+ migrations.CreateModel(
+ name='ResourceStatus',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('timestamp', models.DateTimeField(auto_now_add=True)),
+ ('type', models.CharField(max_length=20)),
+ ('title', models.CharField(max_length=50)),
+ ('content', models.CharField(max_length=5000)),
+ ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dashboard.Resource')),
+ ],
+ options={
+ 'db_table': 'resource_status',
+ },
+ ),
+ migrations.CreateModel(
+ name='Server',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('name', models.CharField(blank=True, max_length=100)),
+ ('model', models.CharField(blank=True, max_length=100)),
+ ('cpu', models.CharField(blank=True, max_length=100)),
+ ('ram', models.CharField(blank=True, max_length=100)),
+ ('storage', models.CharField(blank=True, max_length=100)),
+ ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dashboard.Resource')),
+ ],
+ options={
+ 'db_table': 'server',
+ },
+ ),
+ ]
diff --git a/dashboard/src/dashboard/migrations/0002_auto_20170505_0815.py b/dashboard/src/dashboard/migrations/0002_auto_20170505_0815.py
new file mode 100644
index 0000000..4285b88
--- /dev/null
+++ b/dashboard/src/dashboard/migrations/0002_auto_20170505_0815.py
@@ -0,0 +1,42 @@
+##############################################################################
+# 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
+##############################################################################
+
+
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10 on 2017-05-05 08:15
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='resource',
+ name='dev_pod',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AlterField(
+ model_name='resource',
+ name='owner',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_lab_owner', to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AlterField(
+ model_name='resource',
+ name='slave',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='jenkins.JenkinsSlave'),
+ ),
+ ]
diff --git a/dashboard/src/dashboard/migrations/__init__.py b/dashboard/src/dashboard/migrations/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/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/dashboard/src/dashboard/models.py b/dashboard/src/dashboard/models.py
new file mode 100644
index 0000000..3de7db3
--- /dev/null
+++ b/dashboard/src/dashboard/models.py
@@ -0,0 +1,95 @@
+##############################################################################
+# 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
+from django.db import models
+from django.utils import timezone
+
+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, blank=True)
+ vpn_users = models.ManyToManyField(User, related_name='user_vpn_users', blank=True)
+ slave = models.ForeignKey(JenkinsSlave, on_delete=models.DO_NOTHING, null=True, blank=True)
+ dev_pod = models.BooleanField(default=False)
+
+ 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
+
+class ResourceStatus(models.Model):
+ id = models.AutoField(primary_key=True)
+ resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
+ timestamp = models.DateTimeField(auto_now_add=True)
+ type = models.CharField(max_length=20)
+ title = models.CharField(max_length=50)
+ content = models.CharField(max_length=5000)
+
+ class Meta:
+ db_table = 'resource_status'
+
+ def __str__(self):
+ return self.resource.name + ': ' + self.title + ' ' + str(self.timestamp)
diff --git a/dashboard/src/dashboard/tasks.py b/dashboard/src/dashboard/tasks.py
new file mode 100644
index 0000000..c5ef505
--- /dev/null
+++ b/dashboard/src/dashboard/tasks.py
@@ -0,0 +1,24 @@
+##############################################################################
+# 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 celery import shared_task
+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/dashboard/src/dashboard/templatetags/__init__.py b/dashboard/src/dashboard/templatetags/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/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/dashboard/src/dashboard/templatetags/jenkins_filters.py b/dashboard/src/dashboard/templatetags/jenkins_filters.py
new file mode 100644
index 0000000..e7e1425
--- /dev/null
+++ b/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/dashboard/src/dashboard/templatetags/jira_filters.py b/dashboard/src/dashboard/templatetags/jira_filters.py
new file mode 100644
index 0000000..9a97c1d
--- /dev/null
+++ b/dashboard/src/dashboard/templatetags/jira_filters.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.template.defaultfilters import register
+
+
+@register.filter
+def jira_issue_url(issue):
+ return settings.JIRA_URL + '/browse/' + str(issue)
diff --git a/dashboard/src/dashboard/tests/__init__.py b/dashboard/src/dashboard/tests/__init__.py
new file mode 100644
index 0000000..b5914ce
--- /dev/null
+++ b/dashboard/src/dashboard/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/dashboard/src/dashboard/tests/test_models.py b/dashboard/src/dashboard/tests/test_models.py
new file mode 100644
index 0000000..3a3aeab
--- /dev/null
+++ b/dashboard/src/dashboard/tests/test_models.py
@@ -0,0 +1,69 @@
+##############################################################################
+# 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 math import ceil, floor
+
+from django.test import TestCase
+from django.utils import timezone
+
+from booking.models import *
+from dashboard.models import Resource
+from jenkins.models import JenkinsSlave
+
+
+class ResourceModelTestCase(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)
+
+ def test_booking_utilization(self):
+ utilization = self.res1.get_booking_utilization(1)
+ self.assertTrue(utilization['booked_seconds'] == 0)
+ self.assertTrue(utilization['available_seconds'] == timedelta(weeks=1).total_seconds())
+
+ start = timezone.now() + timedelta(days=1)
+ end = start + timedelta(days=1)
+ booking = Booking.objects.create(start=start, end=end, purpose='test', resource=self.res1,
+ user=self.owner)
+
+ utilization = self.res1.get_booking_utilization(1)
+ booked_seconds = timedelta(days=1).total_seconds()
+ self.assertEqual(utilization['booked_seconds'], booked_seconds)
+
+ utilization = self.res1.get_booking_utilization(-1)
+ self.assertEqual(utilization['booked_seconds'], 0)
+
+ booking.delete()
+ start = timezone.now() - timedelta(days=1)
+ end = start + timedelta(days=2)
+ booking = Booking.objects.create(start=start, end=end, purpose='test', resource=self.res1,
+ user=self.owner)
+ booked_seconds = self.res1.get_booking_utilization(1)['booked_seconds']
+ # use ceil because a fraction of the booked time has already passed now
+ booked_seconds = ceil(booked_seconds)
+ self.assertEqual(booked_seconds, timedelta(days=1).total_seconds())
+
+ booking.delete()
+ start = timezone.now() + timedelta(days=6)
+ end = start + timedelta(days=2)
+ booking = Booking.objects.create(start=start, end=end, purpose='test', resource=self.res1,
+ user=self.owner)
+ booked_seconds = self.res1.get_booking_utilization(1)['booked_seconds']
+ booked_seconds = floor(booked_seconds)
+ self.assertEqual(booked_seconds, timedelta(days=1).total_seconds())
+
+
+
+
+
diff --git a/dashboard/src/dashboard/tests/test_views.py b/dashboard/src/dashboard/tests/test_views.py
new file mode 100644
index 0000000..f5e17c2
--- /dev/null
+++ b/dashboard/src/dashboard/tests/test_views.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.test import TestCase
+from django.urls import reverse
+
+from dashboard.models import Resource
+from jenkins.models import JenkinsSlave
+
+
+class DashboardViewTestCase(TestCase):
+ def setUp(self):
+ self.slave_active = JenkinsSlave.objects.create(name='slave_active', url='x', active=True)
+ self.slave_inactive = JenkinsSlave.objects.create(name='slave_inactive', url='x',
+ active=False)
+ self.res_active = Resource.objects.create(name='res_active', slave=self.slave_active,
+ description='x', url='x')
+ self.res_inactive = Resource.objects.create(name='res_inactive', slave=self.slave_inactive,
+ description='x', url='x')
+
+ def test_booking_utilization_json(self):
+ url = reverse('dashboard:booking_utilization', kwargs={'resource_id': 0, 'weeks': 0})
+ self.assertEqual(self.client.get(url).status_code, 404)
+
+ url = reverse('dashboard:booking_utilization', kwargs={'resource_id': self.res_active.id,
+ 'weeks': 0})
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'data')
+
+ def test_jenkins_utilization_json(self):
+ url = reverse('dashboard:jenkins_utilization', kwargs={'resource_id': 0, 'weeks': 0})
+ self.assertEqual(self.client.get(url).status_code, 404)
+
+ url = reverse('dashboard:jenkins_utilization', kwargs={'resource_id': self.res_active.id,
+ 'weeks': 0})
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'data')
+
+ def test_jenkins_slaves_view(self):
+ url = reverse('dashboard:jenkins_slaves')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertIn(self.slave_active, response.context['slaves'])
+ self.assertNotIn(self.slave_inactive, response.context['slaves'])
+
+ def test_ci_pods_view(self):
+ url = reverse('dashboard:ci_pods')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.context['ci_pods']), 0)
+
+ self.slave_active.ci_slave = True
+ self.slave_inactive.ci_slave = True
+ self.slave_active.save()
+ self.slave_inactive.save()
+
+ response = self.client.get(url)
+ self.assertIn(self.res_active, response.context['ci_pods'])
+ self.assertNotIn(self.res_inactive, response.context['ci_pods'])
+
+ def test_dev_pods_view(self):
+ url = reverse('dashboard:dev_pods')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.context['dev_pods']), 0)
+
diff --git a/dashboard/src/dashboard/urls.py b/dashboard/src/dashboard/urls.py
new file mode 100644
index 0000000..609e5d6
--- /dev/null
+++ b/dashboard/src/dashboard/urls.py
@@ -0,0 +1,41 @@
+##############################################################################
+# 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/dashboard/src/dashboard/views.py b/dashboard/src/dashboard/views.py
new file mode 100644
index 0000000..62a9f83
--- /dev/null
+++ b/dashboard/src/dashboard/views.py
@@ -0,0 +1,141 @@
+##############################################################################
+# 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.filter(active=True)
+ 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, slave__active=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(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'])
+ 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, '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, slave__active=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})