From 4b269fba0ca273dfa3acf44c9f5490f01e0c3d87 Mon Sep 17 00:00:00 2001 From: Trevor Bramwell Date: Fri, 22 Sep 2017 12:23:36 -0700 Subject: Rename pharos-dashboard and pharos-validator As subdirectories of the pharos-tools repo, there is little need to keep the pharos prefix. Change-Id: Ica3d79411f409df638647300036c0664183c2725 Signed-off-by: Trevor Bramwell --- dashboard/src/booking/__init__.py | 10 ++ dashboard/src/booking/admin.py | 17 ++++ dashboard/src/booking/apps.py | 15 +++ dashboard/src/booking/forms.py | 23 +++++ dashboard/src/booking/migrations/0001_initial.py | 68 +++++++++++++ dashboard/src/booking/migrations/__init__.py | 10 ++ dashboard/src/booking/models.py | 77 ++++++++++++++ dashboard/src/booking/tests/__init__.py | 10 ++ dashboard/src/booking/tests/test_models.py | 94 +++++++++++++++++ dashboard/src/booking/tests/test_views.py | 106 ++++++++++++++++++++ dashboard/src/booking/urls.py | 39 ++++++++ dashboard/src/booking/views.py | 122 +++++++++++++++++++++++ 12 files changed, 591 insertions(+) create mode 100644 dashboard/src/booking/__init__.py create mode 100644 dashboard/src/booking/admin.py create mode 100644 dashboard/src/booking/apps.py create mode 100644 dashboard/src/booking/forms.py create mode 100644 dashboard/src/booking/migrations/0001_initial.py create mode 100644 dashboard/src/booking/migrations/__init__.py create mode 100644 dashboard/src/booking/models.py create mode 100644 dashboard/src/booking/tests/__init__.py create mode 100644 dashboard/src/booking/tests/test_models.py create mode 100644 dashboard/src/booking/tests/test_views.py create mode 100644 dashboard/src/booking/urls.py create mode 100644 dashboard/src/booking/views.py (limited to 'dashboard/src/booking') diff --git a/dashboard/src/booking/__init__.py b/dashboard/src/booking/__init__.py new file mode 100644 index 0000000..b5914ce --- /dev/null +++ b/dashboard/src/booking/__init__.py @@ -0,0 +1,10 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + diff --git a/dashboard/src/booking/admin.py b/dashboard/src/booking/admin.py new file mode 100644 index 0000000..d883be1 --- /dev/null +++ b/dashboard/src/booking/admin.py @@ -0,0 +1,17 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.contrib import admin + +from booking.models import * + +admin.site.register(Booking) +admin.site.register(Installer) +admin.site.register(Scenario) \ No newline at end of file diff --git a/dashboard/src/booking/apps.py b/dashboard/src/booking/apps.py new file mode 100644 index 0000000..99bf115 --- /dev/null +++ b/dashboard/src/booking/apps.py @@ -0,0 +1,15 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.apps import AppConfig + + +class BookingConfig(AppConfig): + name = 'booking' diff --git a/dashboard/src/booking/forms.py b/dashboard/src/booking/forms.py new file mode 100644 index 0000000..2dbfacb --- /dev/null +++ b/dashboard/src/booking/forms.py @@ -0,0 +1,23 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +import django.forms as forms + +from booking.models import Installer, Scenario + + +class BookingForm(forms.Form): + fields = ['start', 'end', 'purpose', 'installer', 'scenario'] + + start = forms.DateTimeField() + end = forms.DateTimeField() + purpose = forms.CharField(max_length=300) + installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=False) + scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=False) \ No newline at end of file diff --git a/dashboard/src/booking/migrations/0001_initial.py b/dashboard/src/booking/migrations/0001_initial.py new file mode 100644 index 0000000..6932dae --- /dev/null +++ b/dashboard/src/booking/migrations/0001_initial.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-11-03 13:33 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('dashboard', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Booking', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('start', models.DateTimeField()), + ('end', models.DateTimeField()), + ('jira_issue_id', models.IntegerField(null=True)), + ('jira_issue_status', models.CharField(max_length=50)), + ('purpose', models.CharField(max_length=300)), + ], + options={ + 'db_table': 'booking', + }, + ), + migrations.CreateModel( + name='Installer', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=30)), + ], + ), + migrations.CreateModel( + name='Scenario', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=300)), + ], + ), + migrations.AddField( + model_name='booking', + name='installer', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='booking.Installer'), + ), + 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='scenario', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='booking.Scenario'), + ), + 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/dashboard/src/booking/migrations/__init__.py b/dashboard/src/booking/migrations/__init__.py new file mode 100644 index 0000000..b5914ce --- /dev/null +++ b/dashboard/src/booking/migrations/__init__.py @@ -0,0 +1,10 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + diff --git a/dashboard/src/booking/models.py b/dashboard/src/booking/models.py new file mode 100644 index 0000000..0b3fa3b --- /dev/null +++ b/dashboard/src/booking/models.py @@ -0,0 +1,77 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.conf import settings +from django.contrib.auth.models import User +from django.db import models +from jira import JIRA +from jira import JIRAError + +from dashboard.models import Resource + + +class Installer(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=30) + + def __str__(self): + return self.name + +class Scenario(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=300) + + def __str__(self): + return self.name + + +class Booking(models.Model): + id = models.AutoField(primary_key=True) + user = models.ForeignKey(User, models.CASCADE) # delete if user is deleted + resource = models.ForeignKey(Resource, models.PROTECT) + start = models.DateTimeField() + end = models.DateTimeField() + jira_issue_id = models.IntegerField(null=True) + jira_issue_status = models.CharField(max_length=50) + + installer = models.ForeignKey(Installer, models.DO_NOTHING, null=True) + scenario = models.ForeignKey(Scenario, models.DO_NOTHING, null=True) + purpose = models.CharField(max_length=300, blank=False) + + class Meta: + db_table = 'booking' + + def get_jira_issue(self): + try: + jira = JIRA(server=settings.JIRA_URL, + basic_auth=(settings.JIRA_USER_NAME, settings.JIRA_USER_PASSWORD)) + issue = jira.issue(self.jira_issue_id) + return issue + except JIRAError: + return None + + def 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 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/dashboard/src/booking/tests/__init__.py b/dashboard/src/booking/tests/__init__.py new file mode 100644 index 0000000..b5914ce --- /dev/null +++ b/dashboard/src/booking/tests/__init__.py @@ -0,0 +1,10 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + diff --git a/dashboard/src/booking/tests/test_models.py b/dashboard/src/booking/tests/test_models.py new file mode 100644 index 0000000..b4cd113 --- /dev/null +++ b/dashboard/src/booking/tests/test_models.py @@ -0,0 +1,94 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from datetime import timedelta + +from django.contrib.auth.models import Permission +from django.test import TestCase +from django.utils import timezone + +from booking.models import * +from dashboard.models import Resource +from jenkins.models import JenkinsSlave + + +class 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) + + self.installer = Installer.objects.create(name='TestInstaller') + self.scenario = Scenario.objects.create(name='TestScenario') + + 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, scenario=self.scenario, + installer=self.installer)) \ No newline at end of file diff --git a/dashboard/src/booking/tests/test_views.py b/dashboard/src/booking/tests/test_views.py new file mode 100644 index 0000000..c1da013 --- /dev/null +++ b/dashboard/src/booking/tests/test_views.py @@ -0,0 +1,106 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from datetime import timedelta + +from django.test import Client +from django.test import TestCase +from django.urls import reverse +from django.utils import timezone +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.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) + + + def test_booking_view(self): + start = timezone.now() + end = start + timedelta(weeks=1) + booking = Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1) + + url = reverse('booking:detail', kwargs={'booking_id':0}) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + url = reverse('booking:detail', kwargs={'booking_id':booking.id}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed('booking/booking_detail.html') + self.assertIn('booking', response.context) + + def test_booking_list_view(self): + start = timezone.now() - timedelta(weeks=2) + end = start + timedelta(weeks=1) + Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1) + + url = reverse('booking:list') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed('booking/booking_list.html') + self.assertTrue(len(response.context['bookings']) == 0) + + start = timezone.now() + end = start + timedelta(weeks=1) + Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1) + response = self.client.get(url) + self.assertTrue(len(response.context['bookings']) == 1) \ No newline at end of file diff --git a/dashboard/src/booking/urls.py b/dashboard/src/booking/urls.py new file mode 100644 index 0000000..9e01316 --- /dev/null +++ b/dashboard/src/booking/urls.py @@ -0,0 +1,39 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +"""pharos_dashboard URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url + +from booking.views import * + +urlpatterns = [ + url(r'^(?P[0-9]+)/$', BookingFormView.as_view(), name='create'), + url(r'^(?P[0-9]+)/bookings_json/$', ResourceBookingsJSON.as_view(), + name='bookings_json'), + + url(r'^detail/$', BookingView.as_view(), name='detail_prefix'), + url(r'^detail/(?P[0-9]+)/$', BookingView.as_view(), name='detail'), + + url(r'^list/$', BookingListView.as_view(), name='list') +] diff --git a/dashboard/src/booking/views.py b/dashboard/src/booking/views.py new file mode 100644 index 0000000..6fdca0e --- /dev/null +++ b/dashboard/src/booking/views.py @@ -0,0 +1,122 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import JsonResponse +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.utils import timezone +from django.views import View +from django.views.generic import FormView +from django.views.generic import TemplateView +from jira import JIRAError + +from account.jira_util import get_jira +from booking.forms import BookingForm +from booking.models import Booking +from dashboard.models import Resource + + +def create_jira_ticket(user, booking): + jira = get_jira(user) + issue_dict = { + 'project': 'PHAROS', + 'summary': str(booking.resource) + ': Access Request', + 'description': booking.purpose, + 'issuetype': {'name': 'Task'}, + 'components': [{'name': 'POD Access Request'}], + 'assignee': {'name': booking.resource.owner.username} + } + issue = jira.create_issue(fields=issue_dict) + jira.add_attachment(issue, user.userprofile.pgp_public_key) + jira.add_attachment(issue, user.userprofile.ssh_public_key) + booking.jira_issue_id = issue.id + booking.save() + + +class BookingFormView(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): + if not self.request.user.is_authenticated: + messages.add_message(self.request, messages.ERROR, + 'You need to be logged in to book a Pod.') + return super(BookingFormView, self).form_invalid(form) + + user = self.request.user + booking = Booking(start=form.cleaned_data['start'], + end=form.cleaned_data['end'], + purpose=form.cleaned_data['purpose'], + installer=form.cleaned_data['installer'], + scenario=form.cleaned_data['scenario'], + 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) + try: + if settings.CREATE_JIRA_TICKET: + create_jira_ticket(user, booking) + except JIRAError: + messages.add_message(self.request, messages.ERROR, 'Failed to create Jira Ticket. ' + 'Please check your Jira ' + 'permissions.') + booking.delete() + return super(BookingFormView, self).form_invalid(form) + messages.add_message(self.request, messages.SUCCESS, 'Booking saved') + return super(BookingFormView, self).form_valid(form) + + +class BookingView(TemplateView): + template_name = "booking/booking_detail.html" + + def get_context_data(self, **kwargs): + booking = get_object_or_404(Booking, id=self.kwargs['booking_id']) + title = 'Booking Details' + context = super(BookingView, self).get_context_data(**kwargs) + context.update({'title': title, 'booking': booking}) + return context + + +class BookingListView(TemplateView): + template_name = "booking/booking_list.html" + + def get_context_data(self, **kwargs): + bookings = Booking.objects.filter(end__gte=timezone.now()) + title = 'Search Booking' + context = super(BookingListView, self).get_context_data(**kwargs) + context.update({'title': title, 'bookings': bookings}) + return context + + +class ResourceBookingsJSON(View): + def get(self, request, *args, **kwargs): + resource = get_object_or_404(Resource, id=self.kwargs['resource_id']) + bookings = resource.booking_set.get_queryset().values('id', 'start', 'end', 'purpose', + 'jira_issue_status', + 'installer__name', 'scenario__name') + return JsonResponse({'bookings': list(bookings)}) -- cgit 1.2.3-korg