Use Jira Oauth for user authentication 75/19075/1
authormaxbr <maxbr@mi.fu-berlin.de>
Fri, 19 Aug 2016 15:15:28 +0000 (17:15 +0200)
committermaxbr <maxbr@mi.fu-berlin.de>
Fri, 19 Aug 2016 15:15:28 +0000 (17:15 +0200)
JIRA: RELENG-12

Users can use their jira accounts for the dashboard. This also allows
the dasboard to open jira tickets for bookings.

Signed-off-by: maxbr <maxbr@mi.fu-berlin.de>
25 files changed:
tools/pharos-dashboard/account/forms.py
tools/pharos-dashboard/account/jira_util.py [new file with mode: 0644]
tools/pharos-dashboard/account/migrations/0002_auto_20160816_1511.py [new file with mode: 0644]
tools/pharos-dashboard/account/migrations/0003_auto_20160819_1024.py [new file with mode: 0644]
tools/pharos-dashboard/account/migrations/0004_auto_20160819_1055.py [new file with mode: 0644]
tools/pharos-dashboard/account/models.py
tools/pharos-dashboard/account/rsa.pem [new file with mode: 0644]
tools/pharos-dashboard/account/rsa.pub [new file with mode: 0644]
tools/pharos-dashboard/account/urls.py
tools/pharos-dashboard/account/views.py
tools/pharos-dashboard/booking/tests/test_views.py
tools/pharos-dashboard/booking/views.py
tools/pharos-dashboard/dashboard/migrations/0006_delete_resourceutilization.py [new file with mode: 0644]
tools/pharos-dashboard/dashboard/models.py
tools/pharos-dashboard/dashboard/urls.py
tools/pharos-dashboard/dashboard/views.py
tools/pharos-dashboard/jenkins/adapter.py
tools/pharos-dashboard/pharos_dashboard/settings.py
tools/pharos-dashboard/pharos_dashboard/urls.py
tools/pharos-dashboard/static/css/theme.css
tools/pharos-dashboard/static/js/fullcalendar-options.js
tools/pharos-dashboard/templates/account/userprofile_update_form.html [new file with mode: 0644]
tools/pharos-dashboard/templates/base.html
tools/pharos-dashboard/templates/booking/booking_calendar.html
tools/pharos-dashboard/templates/dashboard/lab_owner.html [new file with mode: 0644]

index 7893867..14f11cd 100644 (file)
@@ -1,17 +1,14 @@
 import django.forms as forms
 import pytz as pytz
 
-from registration.forms import RegistrationForm as BaseRegistrationForm
+from account.models import UserProfile
 
 
-class AccountSettingsForm(forms.Form):
-    fields = ['first_name', 'last_name', 'email', 'company', 'ssh_public_key', 'pgp_public_key',
-              'timezone']
+class AccountSettingsForm(forms.ModelForm):
+    class Meta:
+        model = UserProfile
+        fields = ['company', 'ssh_public_key', 'pgp_public_key', 'timezone']
 
-    first_name = forms.CharField(max_length=30)
-    last_name = forms.CharField(max_length=30)
-    email = forms.EmailField()
-    company = forms.CharField(max_length=30)
     ssh_public_key = forms.CharField(max_length=2048, widget=forms.Textarea)
     pgp_public_key = forms.CharField(max_length=2048, widget=forms.Textarea)
-    timezone = forms.ChoiceField(choices=[(x, x) for x in pytz.common_timezones], initial='UTC')
\ No newline at end of file
+    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 (file)
index 0000000..bd07ff3
--- /dev/null
@@ -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/migrations/0002_auto_20160816_1511.py b/tools/pharos-dashboard/account/migrations/0002_auto_20160816_1511.py
new file mode 100644 (file)
index 0000000..3fcd989
--- /dev/null
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10 on 2016-08-16 15:11
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('account', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='userprofile',
+            name='oauth_secret',
+            field=models.CharField(default='', max_length=1024),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='userprofile',
+            name='oauth_token',
+            field=models.CharField(default='', max_length=1024),
+            preserve_default=False,
+        ),
+    ]
diff --git a/tools/pharos-dashboard/account/migrations/0003_auto_20160819_1024.py b/tools/pharos-dashboard/account/migrations/0003_auto_20160819_1024.py
new file mode 100644 (file)
index 0000000..b648844
--- /dev/null
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10 on 2016-08-19 10:24
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('account', '0002_auto_20160816_1511'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='userprofile',
+            old_name='pgpkey',
+            new_name='pgp_pupblic_key',
+        ),
+        migrations.RenameField(
+            model_name='userprofile',
+            old_name='sshkey',
+            new_name='ssh_public_key',
+        ),
+    ]
diff --git a/tools/pharos-dashboard/account/migrations/0004_auto_20160819_1055.py b/tools/pharos-dashboard/account/migrations/0004_auto_20160819_1055.py
new file mode 100644 (file)
index 0000000..51af0aa
--- /dev/null
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10 on 2016-08-19 10:55
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('account', '0003_auto_20160819_1024'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='userprofile',
+            old_name='pgp_pupblic_key',
+            new_name='pgp_public_key',
+        ),
+    ]
index 5181c71..fbabf6c 100644 (file)
@@ -8,9 +8,11 @@ from dashboard.models import Resource
 class UserProfile(models.Model):
     user = models.OneToOneField(User, on_delete=models.CASCADE)
     timezone = models.CharField(max_length=100, blank=False, default='UTC')
-    sshkey = models.CharField(max_length=2048, blank=False)
-    pgpkey = models.CharField(max_length=2048, blank=False)
+    ssh_public_key = models.CharField(max_length=2048, blank=False)
+    pgp_public_key = models.CharField(max_length=2048, blank=False)
     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 (file)
index 0000000..dbd4eed
--- /dev/null
@@ -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 (file)
index 0000000..cc50e45
--- /dev/null
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0YjCwIfYoprq/FQO6lb3asXrx
+LlJFuCvtinTF5p0GxvQGu5O3gYytUvtC2JlYzypSRjVxwxrsuRcP3e641SdASwfr
+mzyvIgP08N4S0IFzEURkV1wp/IpH7kH41EtbmUmrXSwfNZsnQRE5SYSOhh+LcK2w
+yQkdgcMv11l4KoBkcwIDAQAB
+-----END PUBLIC KEY-----
index 5d68135..b837814 100644 (file)
@@ -14,13 +14,12 @@ Including another URLconf
     2. Add a URL to urlpatterns:  url(r'^blog/', include('blog.urls'))
 """
 from django.conf.urls import url
-from django.contrib.auth import views as auth_views
 
 from account.views import *
 
 urlpatterns = [
-    url(r'^login/$', auth_views.login, name='login'),
-    url(r'^logout/$', auth_views.logout, name='logout'),
-    url(r'^register/', RegistrationView.as_view(), name='registration'),
     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')
 ]
index 3432867..7d2c9bd 100644 (file)
+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.core.exceptions import ObjectDoesNotExist
+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 FormView
-from registration.backends.simple.views import RegistrationView as BaseRegistrationView
+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 '/'
 
-class RegistrationView(BaseRegistrationView):
-    template_name = 'registration/registration_form.html'
+    def get_object(self, queryset=None):
+        return self.request.user.userprofile
 
-    def get_context_data(self, **kwargs):
-        context = super(RegistrationView, self).get_context_data(**kwargs)
-        context.update({'title': "Registration"})
-        return context
 
-    def register(self, form):
-        new_user = super(RegistrationView, self).register(form)
-        UserProfile.objects.create(user=new_user)
-        messages.add_message(self.request, messages.INFO, 'Please complete your user profile.')
-        return new_user
+class JiraLoginView(RedirectView):
+    def get_redirect_url(self, *args, **kwargs):
+        client = oauth.Client(consumer)
+        client.set_signature_method(SignatureMethod_RSA_SHA1())
 
-    def get_success_url(self, user):
-        return reverse('account:settings')
+        # 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
 
-@method_decorator(login_required, name='dispatch')
-class AccountSettingsView(FormView):
-    form_class = AccountSettingsForm
-    template_name = 'registration/registration_form.html'
-    success_url = '/'
 
-    def dispatch(self, request, *args, **kwargs):
+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:
-            request.user.userprofile
-        except ObjectDoesNotExist:
-            UserProfile.objects.create(user=request.user)
-            messages.add_message(self.request, messages.INFO,
-                                 'Please complete your user profile to proceed.')
-        return super(AccountSettingsView, self).dispatch(request, *args, **kwargs)
-
-    def get_context_data(self, **kwargs):
-        context = super(AccountSettingsView, self).get_context_data(**kwargs)
-        context.update({'title': "Settings"})
-        return context
-
-    def get_initial(self):
-        user = self.request.user
-        initial = super(AccountSettingsView, self).get_initial()
-        initial['first_name'] = user.first_name
-        initial['last_name'] = user.last_name
-        initial['email'] = user.email
-        initial['company'] = user.userprofile.company
-        initial['ssh_public_key'] = user.userprofile.sshkey
-        initial['pgp_public_key'] = user.userprofile.pgpkey
-        initial['timezone'] = user.userprofile.timezone
-        return initial
-
-    def form_valid(self, form):
-        user = self.request.user
-        user.first_name = form.cleaned_data['first_name']
-        user.last_name = form.cleaned_data['last_name']
-        user.email = form.cleaned_data['email']
-        user.userprofile.company = form.cleaned_data['company']
-        user.userprofile.sshkey = form.cleaned_data['ssh_public_key']
-        user.userprofile.pgpkey = form.cleaned_data['pgp_public_key']
-        user.userprofile.timezone = form.cleaned_data['timezone']
+            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()
-        if not user.is_active:
-            user.is_active = True
+        user.set_password(access_token['oauth_token_secret'])
         user.save()
-        messages.add_message(self.request, messages.INFO,
-                             'Settings saved')
-        return super(AccountSettingsView, self).form_valid(form)
+        user = authenticate(username=username, password=access_token['oauth_token_secret'])
+        login(self.request, user)
+        # redirect user to settings page to complete profile
+        return url
index 4f5ee8b..b0c4b49 100644 (file)
@@ -56,13 +56,8 @@ class BookingViewTestCase(TestCase):
         url = reverse('booking:create', kwargs={'resource_id': 0})
         self.assertEqual(self.client.get(url).status_code, 404)
 
-        # anonymous user
-        url = reverse('booking:create', kwargs={'resource_id': self.res1.id})
-        response = self.client.get(url, follow=True)
-        self.assertRedirects(response, reverse('account:login') + '?next=/booking/' + str(
-            self.res1.id) + '/')
-
         # 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)
index bc00d3e..c461aef 100644 (file)
@@ -6,17 +6,29 @@ from django.urls import reverse
 from django.views import View
 from django.views.generic import FormView
 
+from account.jira_util import get_jira
 from booking.forms import BookingForm
 from booking.models import Booking
 from dashboard.models import Resource
 
+
 class BookingFormView(LoginRequiredMixin, FormView):
     template_name = "booking/booking_calendar.html"
     form_class = BookingForm
 
+    def open_jira_issue(self,booking):
+        jira = get_jira(self.request.user)
+        issue_dict = {
+            'project': 'PHAROS',
+            'summary': 'Booking: ' + str(self.resource),
+            'description': str(booking),
+            'issuetype': {'name': 'Task'},
+        }
+        jira.create_issue(fields=issue_dict)
+
     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)
+        return super(BookingFormView, self).dispatch(request, *args, **kwargs)
 
     def get_context_data(self, **kwargs):
         title = 'Booking: ' + self.resource.name
@@ -39,6 +51,7 @@ class BookingFormView(LoginRequiredMixin, FormView):
         except PermissionError as err:
             messages.add_message(self.request, messages.ERROR, err)
             return super(BookingFormView, self).form_invalid(form)
+        self.open_jira_issue(booking)
         messages.add_message(self.request, messages.SUCCESS, 'Booking saved')
         return super(BookingFormView, self).form_valid(form)
 
diff --git a/tools/pharos-dashboard/dashboard/migrations/0006_delete_resourceutilization.py b/tools/pharos-dashboard/dashboard/migrations/0006_delete_resourceutilization.py
new file mode 100644 (file)
index 0000000..fb637bd
--- /dev/null
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10 on 2016-08-16 10:42
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dashboard', '0005_remove_resource_slavename'),
+    ]
+
+    operations = [
+        migrations.DeleteModel(
+            name='ResourceUtilization',
+        ),
+    ]
index cb6b92b..02073e6 100644 (file)
@@ -17,16 +17,4 @@ class Resource(models.Model):
         db_table = 'resource'
 
     def __str__(self):
-        return self.name
-
-
-class ResourceUtilization(models.Model):
-    POD_STATUS = {
-        'online': 1,
-        'idle': 2,
-        'offline': 3
-    }
-
-    id = models.AutoField(primary_key=True)
-    timestamp = models.DateTimeField(auto_created=True)
-    pod_status = models.IntegerField()
+        return self.name
\ No newline at end of file
index 2223e39..51d764c 100644 (file)
@@ -21,8 +21,8 @@ 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/utilization$', ResourceUtilizationView.as_view(),
-        name='resource_utilization'),
+    url(r'^resource/all/', LabOwnerView.as_view(),
+        name='resources'),
 
     url(r'^$', DevelopmentPodsView.as_view(), name="index"),
 ]
index da31802..6af2c1a 100644 (file)
@@ -4,7 +4,6 @@ from django.views.generic import TemplateView
 
 from booking.models import Booking
 from dashboard.models import Resource
-from jenkins import adapter as jenkins
 from jenkins.models import JenkinsSlave, JenkinsStatistic
 
 
@@ -50,11 +49,11 @@ class DevelopmentPodsView(TemplateView):
         return context
 
 
-class ResourceUtilizationView(TemplateView):
-    template_name = "dashboard/resource_utilization.html"
+class LabOwnerView(TemplateView):
+    template_name = "dashboard/lab_owner.html"
 
     def get_context_data(self, **kwargs):
-        resources = Resource.objects.all()
+        resources = Resource.objects.filter(slave__dev_pod=True)
         pods = []
         for resource in resources:
             utilization = {'idle': 0, 'online': 0, 'offline': 0}
@@ -62,12 +61,14 @@ class ResourceUtilizationView(TemplateView):
             statistics = JenkinsStatistic.objects.filter(slave=resource.slave,
                                                          timestamp__gte=timezone.now() - timedelta(
                                                              days=7))
-            statistics_cnt = statistics.count()
-            if statistics_cnt != 0:
-                utilization['idle'] = statistics.filter(idle=True).count()
-                utilization['online'] = statistics.filter(online=True).count()
-                utilization['offline'] = statistics.filter(offline=True).count()
-            pods.append((resource, utilization))
-        context = super(ResourceUtilizationView, self).get_context_data(**kwargs)
-        context.update({'title': "Development Pods", 'pods': pods})
+
+            utilization['idle'] = statistics.filter(idle=True).count()
+            utilization['online'] = statistics.filter(online=True).count()
+            utilization['offline'] = statistics.filter(offline=True).count()
+
+            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
index fabd535..f9e352a 100644 (file)
@@ -6,7 +6,6 @@ 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:
index 7717501..a482f95 100644 (file)
@@ -56,6 +56,7 @@ MIDDLEWARE = [
     'account.middleware.TimezoneMiddleware',
 ]
 
+
 ROOT_URLCONF = 'pharos_dashboard.urls'
 
 TEMPLATES = [
@@ -144,3 +145,11 @@ djcelery.setup_loader()
 BROKER_URL = 'django://'
 CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler'
 
+JIRA_URL = 'http://localhost:8080'
+
+OAUTH_CONSUMER_KEY = 'oauth-pharos-dashboard-consumer'
+OAUTH_CONSUMER_SECRET = 'development_secret'
+
+OAUTH_REQUEST_TOKEN_URL = JIRA_URL + '/plugins/servlet/oauth/request-token'
+OAUTH_ACCESS_TOKEN_URL = JIRA_URL + '/plugins/servlet/oauth/access-token'
+OAUTH_AUTHORIZE_URL = JIRA_URL + '/plugins/servlet/oauth/authorize'
\ No newline at end of file
index 41aa409..26ab367 100644 (file)
@@ -20,5 +20,6 @@ urlpatterns = [
     url(r'^', include('dashboard.urls', namespace='dashboard')),
     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
index 4cec341..bd15637 100644 (file)
@@ -1,7 +1,13 @@
 .blink_me {
-  animation: blinker 1.5s linear infinite;
+    animation: blinker 1.5s linear infinite;
 }
 
 @keyframes blinker {
-  20% { opacity: 0.4; }
+    20% {
+        opacity: 0.4;
+    }
+}
+
+.modal p {
+    word-wrap: break-word;
 }
\ No newline at end of file
index 85423b8..c57baa6 100644 (file)
@@ -1,14 +1,8 @@
 var tmpevent;
 
-// converts a moment to a readable fomat for the backend
-function convertInputTime(time) {
-    return time;
-    //return moment(time).format('YYYY-MM-DD HH:00 ZZ');
-}
-
 function sendEventToForm(event) {
-    $('#starttimepicker').data("DateTimePicker").date(convertInputTime(event.start));
-    $('#endtimepicker').data("DateTimePicker").date(convertInputTime(event.end));
+    $('#starttimepicker').data("DateTimePicker").date(event.start);
+    $('#endtimepicker').data("DateTimePicker").date(event.end);
 }
 
 var calendarOptions = {
@@ -40,12 +34,13 @@ var calendarOptions = {
         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 = convertInputTime(start);
-        end = convertInputTime(end);
+        start = moment(start);
+        end = moment(end);
 
         tmpevent = {
             id: '537818f62bc63518ece15338fb86c8be',
@@ -64,6 +59,7 @@ var calendarOptions = {
             if (event.id != tmpevent.id) {
                 $('#calendar').fullCalendar('removeEvents', tmpevent.id);
                 $('#calendar').fullCalendar('rerenderEvents');
+                tmpevent = undefined;
             }
         }
     },
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 (file)
index 0000000..0a921d5
--- /dev/null
@@ -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 method="post" action="">
+                            {% csrf_token %}
+                            {% bootstrap_form form %}
+                            {% buttons %}
+                                <button type="submit" class="btn btn btn-success">
+                                    Submit
+                                </button>
+                            {% endbuttons %}
+                        </form>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+{% endblock basecontent %}
index 1479b04..64174a1 100644 (file)
                         {% else %}
                             <li><a href="{% url 'account:login' %}"><i
                                     class="fa fa-sign-in fa-fw"></i>
-                                Login</a>
+                                Login with Jira</a>
                             <li>
-                            <a href="{% url 'account:registration' %}"><i
-                                    class="fa fa-edit fa-fw"></i>
-                                Register</a>
                         {% endif %}
                     </ul>
                     <!-- /.dropdown-user -->
                     <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 -->
index 1fa5dc4..d144bb8 100644 (file)
@@ -1,6 +1,8 @@
 {% extends "dashboard/table.html" %}
 {% load staticfiles %}
 
+{% load bootstrap3 %}
+
 {% block extrahead %}
     <link href="{% static "bower_components/fullcalendar/dist/fullcalendar.css" %}"
           rel='stylesheet'/>
             </div>
             <div class="panel-body">
                 <div id="booking_form_div">
-                    {% include 'booking/booking_form.html' %}
+                    {% bootstrap_form_errors form type='non_fields' %}
+                    <form method="post" action="" class="form" id="bookingform">
+                        {% csrf_token %}
+
+                        <div class='input-group' id='starttimepicker'>
+                            {% bootstrap_field form.start addon_after='<span class="glyphicon glyphicon-calendar"></span>' %}
+                        </div>
+                        <div class='input-group' id='endtimepicker'>
+                            {% bootstrap_field form.end addon_after='<span class="glyphicon glyphicon-calendar"></span>' %}
+                        </div>
+                        {% bootstrap_field form.purpose %}
+
+                        {% buttons %}
+                            <button type="submit" class="btn btn btn-success">
+                                Book
+                            </button>
+                        {% endbuttons %}
+                    </form>
                 </div>
             </div>
         </div>
diff --git a/tools/pharos-dashboard/templates/dashboard/lab_owner.html b/tools/pharos-dashboard/templates/dashboard/lab_owner.html
new file mode 100644 (file)
index 0000000..a4f428c
--- /dev/null
@@ -0,0 +1,151 @@
+{% 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-3">
+                <div class="panel panel-default">
+                    <div class="panel-heading">
+                        {{ resource.name }}
+                    </div>
+                    <div class="panel-body">
+                        <div class="flot-chart">
+                            <div class="flot-chart-content" id="{{ resource.slave.name }}"></div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="col-lg-6">
+                <div class="panel panel-default">
+                    <div class="panel-heading">
+                        {{ resource.name }} Bookings
+                    </div>
+                    <div class="panel-body">
+                        <div class="dataTables_wrapper">
+                            <table class="table table-striped table-bordered table-hover"
+                                   id="{{ resource.slave.name }}_bookings" cellspacing="0"
+                                   width="100%">
+                                <thead>
+                                <tr>
+                                    <th>User</th>
+                                    <th>Purpose</th>
+                                    <th>Start</th>
+                                    <th>End</th>
+                                    <th>Status</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>
+                                            Jira Status
+                                        </th>
+                                    </tr>
+                                {% endfor %}`
+                                </tbody>
+                            </table>
+                        </div>
+                    </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.slave.name }}_bookings').DataTable({});
+
+                $(function () {
+                    var data = [{
+                        label: "Offline",
+                        data: {{ utilization.offline }},
+                        color: '#d9534f'
+                    }, {
+                        label: "Online",
+                        data: {{ utilization.online }},
+                        color: '#5cb85c'
+                    }, {
+                        label: "Idle",
+                        data: {{ utilization.idle }},
+                        color: '#5bc0de'
+                    }];
+
+                    var plotObj = $.plot($("#{{ resource.slave.name }}"), data, {
+                        series: {
+                            pie: {
+                                show: true
+                            }
+                        },
+                        grid: {
+                            hoverable: false
+                        },
+                        tooltip: true,
+                        tooltipOpts: {
+                            content: "%p.0%, %s", // show percentages, rounding to 2 decimal places
+                            shifts: {
+                                x: 20,
+                                y: 0
+                            },
+                            defaultTheme: false
+                        }
+                    });
+
+                });
+            {% endfor %}
+
+        });
+    </script>
+
+{% endblock extrajs %}
\ No newline at end of file