diff options
author | maxbr <maxbr@mi.fu-berlin.de> | 2016-08-19 17:15:28 +0200 |
---|---|---|
committer | maxbr <maxbr@mi.fu-berlin.de> | 2016-08-19 17:15:28 +0200 |
commit | ebaa05ab2b53634a7a3e738618a031fd1518d796 (patch) | |
tree | 2df4bf769f6ed52f4b9109a96d3194aa5a95e453 /tools | |
parent | 54322038f766be460b676d60974886be5f04d8f3 (diff) |
Use Jira Oauth for user authentication
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>
Diffstat (limited to 'tools')
25 files changed, 540 insertions, 130 deletions
diff --git a/tools/pharos-dashboard/account/forms.py b/tools/pharos-dashboard/account/forms.py index 7893867f..14f11cda 100644 --- a/tools/pharos-dashboard/account/forms.py +++ b/tools/pharos-dashboard/account/forms.py @@ -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 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/migrations/0002_auto_20160816_1511.py b/tools/pharos-dashboard/account/migrations/0002_auto_20160816_1511.py new file mode 100644 index 00000000..3fcd989a --- /dev/null +++ b/tools/pharos-dashboard/account/migrations/0002_auto_20160816_1511.py @@ -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 index 00000000..b648844e --- /dev/null +++ b/tools/pharos-dashboard/account/migrations/0003_auto_20160819_1024.py @@ -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 index 00000000..51af0aa9 --- /dev/null +++ b/tools/pharos-dashboard/account/migrations/0004_auto_20160819_1055.py @@ -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', + ), + ] diff --git a/tools/pharos-dashboard/account/models.py b/tools/pharos-dashboard/account/models.py index 5181c719..fbabf6c4 100644 --- a/tools/pharos-dashboard/account/models.py +++ b/tools/pharos-dashboard/account/models.py @@ -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 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/urls.py b/tools/pharos-dashboard/account/urls.py index 5d681357..b837814a 100644 --- a/tools/pharos-dashboard/account/urls.py +++ b/tools/pharos-dashboard/account/urls.py @@ -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') ] diff --git a/tools/pharos-dashboard/account/views.py b/tools/pharos-dashboard/account/views.py index 34328674..7d2c9bd0 100644 --- a/tools/pharos-dashboard/account/views.py +++ b/tools/pharos-dashboard/account/views.py @@ -1,78 +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.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 diff --git a/tools/pharos-dashboard/booking/tests/test_views.py b/tools/pharos-dashboard/booking/tests/test_views.py index 4f5ee8bd..b0c4b498 100644 --- a/tools/pharos-dashboard/booking/tests/test_views.py +++ b/tools/pharos-dashboard/booking/tests/test_views.py @@ -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) diff --git a/tools/pharos-dashboard/booking/views.py b/tools/pharos-dashboard/booking/views.py index bc00d3eb..c461aef4 100644 --- a/tools/pharos-dashboard/booking/views.py +++ b/tools/pharos-dashboard/booking/views.py @@ -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 index 00000000..fb637bd7 --- /dev/null +++ b/tools/pharos-dashboard/dashboard/migrations/0006_delete_resourceutilization.py @@ -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', + ), + ] diff --git a/tools/pharos-dashboard/dashboard/models.py b/tools/pharos-dashboard/dashboard/models.py index cb6b92b3..02073e6f 100644 --- a/tools/pharos-dashboard/dashboard/models.py +++ b/tools/pharos-dashboard/dashboard/models.py @@ -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 diff --git a/tools/pharos-dashboard/dashboard/urls.py b/tools/pharos-dashboard/dashboard/urls.py index 2223e39c..51d764c4 100644 --- a/tools/pharos-dashboard/dashboard/urls.py +++ b/tools/pharos-dashboard/dashboard/urls.py @@ -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"), ] diff --git a/tools/pharos-dashboard/dashboard/views.py b/tools/pharos-dashboard/dashboard/views.py index da31802f..6af2c1ad 100644 --- a/tools/pharos-dashboard/dashboard/views.py +++ b/tools/pharos-dashboard/dashboard/views.py @@ -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 diff --git a/tools/pharos-dashboard/jenkins/adapter.py b/tools/pharos-dashboard/jenkins/adapter.py index fabd5356..f9e352a1 100644 --- a/tools/pharos-dashboard/jenkins/adapter.py +++ b/tools/pharos-dashboard/jenkins/adapter.py @@ -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: diff --git a/tools/pharos-dashboard/pharos_dashboard/settings.py b/tools/pharos-dashboard/pharos_dashboard/settings.py index 77175016..a482f95d 100644 --- a/tools/pharos-dashboard/pharos_dashboard/settings.py +++ b/tools/pharos-dashboard/pharos_dashboard/settings.py @@ -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 diff --git a/tools/pharos-dashboard/pharos_dashboard/urls.py b/tools/pharos-dashboard/pharos_dashboard/urls.py index 41aa4093..26ab3677 100644 --- a/tools/pharos-dashboard/pharos_dashboard/urls.py +++ b/tools/pharos-dashboard/pharos_dashboard/urls.py @@ -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 diff --git a/tools/pharos-dashboard/static/css/theme.css b/tools/pharos-dashboard/static/css/theme.css index 4cec341d..bd156372 100644 --- a/tools/pharos-dashboard/static/css/theme.css +++ b/tools/pharos-dashboard/static/css/theme.css @@ -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 diff --git a/tools/pharos-dashboard/static/js/fullcalendar-options.js b/tools/pharos-dashboard/static/js/fullcalendar-options.js index 85423b86..c57baa6f 100644 --- a/tools/pharos-dashboard/static/js/fullcalendar-options.js +++ b/tools/pharos-dashboard/static/js/fullcalendar-options.js @@ -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 index 00000000..0a921d51 --- /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 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 %} diff --git a/tools/pharos-dashboard/templates/base.html b/tools/pharos-dashboard/templates/base.html index 1479b043..64174a1f 100644 --- a/tools/pharos-dashboard/templates/base.html +++ b/tools/pharos-dashboard/templates/base.html @@ -39,11 +39,8 @@ {% 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 --> @@ -57,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/booking/booking_calendar.html b/tools/pharos-dashboard/templates/booking/booking_calendar.html index 1fa5dc4d..d144bb83 100644 --- a/tools/pharos-dashboard/templates/booking/booking_calendar.html +++ b/tools/pharos-dashboard/templates/booking/booking_calendar.html @@ -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'/> @@ -33,7 +35,24 @@ </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 index 00000000..a4f428c7 --- /dev/null +++ b/tools/pharos-dashboard/templates/dashboard/lab_owner.html @@ -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 |