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