diff --git a/adjutant_ui/api/adjutant.py b/adjutant_ui/api/adjutant.py index e17fcc4..40908c7 100644 --- a/adjutant_ui/api/adjutant.py +++ b/adjutant_ui/api/adjutant.py @@ -101,6 +101,14 @@ DEFAULT_HIDDEN_QUOTAS = { ], } +NOTIFICATION = collections.namedtuple('Notification', + ['uuid', 'notes', 'error', 'created_on', + 'acknowledged', 'task']) + + +class AdjutantApiError(BaseException): + pass + def _get_endpoint_url(request): # If the request is made by an anonymous user, this endpoint request fails. @@ -333,6 +341,76 @@ def signup_submit(request, data): raise +def notification_list(request, filters={}, page=1): + notifs_per_page = utils.get_page_size(request) + headers = {"Content-Type": "application/json", + 'X-Auth-Token': request.user.token.id} + + response = get(request, 'notifications', headers=headers, + params={'filters': json.dumps(filters), 'page': page, + 'notifications_per_page': notifs_per_page}) + if not response.status_code == 200: + if response.json() == {'error': 'Empty page'}: + raise AdjutantApiError("Empty Page") + raise BaseException + + notificationlist = [] + for notification in response.json()['notifications']: + notificationlist.append(notification_obj_get( + request, notification=notification)) + has_more = response.json()['has_more'] + has_prev = response.json()['has_prev'] + return notificationlist, has_prev, has_more + + +def notification_get(request, uuid): + headers = {"Content-Type": "application/json", + 'X-Auth-Token': request.user.token.id} + + response = get(request, 'notifications/%s/' % uuid, headers=headers) + return response + + +def notification_obj_get(request, notification_id=None, notification=None): + if not notification: + notification = notification_get(request, notification_id).json() + + if notification['error']: + notes = notification['notes'].get('errors') + else: + notes = notification['notes'].get('notes') + + if not notes: + notes = notification['notes'] + if isinstance(notes, list) and len(notes) == 1: + notes = notes[0] + + if not isinstance(notes, six.text_type): + notes = json.dumps(notes) + + return NOTIFICATION(uuid=notification['uuid'], + task=notification['task'], + error=notification['error'], + created_on=notification['created_on'], + acknowledged=notification['acknowledged'], + notes=notes) + + +def notifications_acknowlege(request, notification_id=None): + headers = {"Content-Type": "application/json", + 'X-Auth-Token': request.user.token.id} + # Takes either a single notification id or a list of them + # and acknowleges all of them + if isinstance(notification_id, list): + data = {'notifications': notification_id} + return post(request, 'notifications', data=json.dumps(data), + headers=headers) + else: + url = "notifications/%s/" % notification_id + return post(request, url, data=json.dumps({'acknowledged': True}), + headers=headers) + + def task_list(request, filters={}, page=1): tasks_per_page = utils.get_page_size(request) tasklist = [] @@ -368,7 +446,7 @@ def task_get(request, task_id): def task_obj_get(request, task_id=None, task=None, page=0): if not task: - task = task_get(request, task_id) + task = task_get(request, task_id).json() status = "Awaiting Approval" if task['cancelled']: @@ -384,8 +462,8 @@ def task_obj_get(request, task_id=None, task=None, page=0): id=task['uuid'], task_type=task['task_type'], valid=valid, - request_by=task['keystone_user'].get('username'), - request_project=task['keystone_user'].get('project_name'), + request_by=task['keystone_user'].get('username', '-'), + request_project=task['keystone_user'].get('project_name', '-'), status=status, created_on=task['created_on'], approved_on=task['approved_on'], diff --git a/adjutant_ui/content/notifications/__init__.py b/adjutant_ui/content/notifications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/adjutant_ui/content/notifications/panel.py b/adjutant_ui/content/notifications/panel.py new file mode 100644 index 0000000..0952470 --- /dev/null +++ b/adjutant_ui/content/notifications/panel.py @@ -0,0 +1,23 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class NotificationPanel(horizon.Panel): + name = _('Admin Notifications') + slug = 'notifications' + policy_rules = (("identity", "admin_required"),) diff --git a/adjutant_ui/content/notifications/tables.py b/adjutant_ui/content/notifications/tables.py new file mode 100644 index 0000000..d1a0930 --- /dev/null +++ b/adjutant_ui/content/notifications/tables.py @@ -0,0 +1,111 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from django.core import urlresolvers +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from horizon import exceptions +from horizon import tables + +from adjutant_ui.api import adjutant + + +class AcknowlegeNotifcation(tables.BatchAction): + name = 'acknowlege' + help_text = _("This will acknowlege all selected tasks.") + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Acknowlege Notification", + u"Acknowlege Notifications", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Acknowleged Notification", + u"Acknowleged Notifications", + count + ) + + def action(self, request, obj_id): + result = adjutant.notifications_acknowlege(request, obj_id) + if not result or result.status_code != 200: + exception = exceptions.NotAvailable() + exception._safe_message = False + raise exception + + def allowed(self, request, notification=None): + if notification: + return not(notification.acknowledged) + return True + + +def get_task_link(datum): + return urlresolvers.reverse("horizon:management:tasks:detail", + args=(datum.task,)) + + +class ErrorRow(tables.Row): + + def __init__(self, *args, **kwargs): + super(ErrorRow, self).__init__(*args, **kwargs) + if not self.datum: + return + if self.datum.error: + self.classes.append('danger') + + +class NotificationTable(tables.DataTable): + uuid = tables.Column('uuid', verbose_name=_('Notification ID'), + link="horizon:management:notifications:detail") + task = tables.Column('task', verbose_name=_('Task ID'), + link=get_task_link) + error = tables.Column('error', verbose_name=_('Error')) + created_on = tables.Column('created_on', + verbose_name=_('Request Date')) + notes = tables.Column('notes') + + class Meta(object): + template = 'notifications/table_override.html' + name = 'notification_table' + verbose_name = _('Unacknowledged Notifications') + table_actions = (AcknowlegeNotifcation, ) + row_actions = (AcknowlegeNotifcation, ) + row_class = ErrorRow + prev_pagination_param = pagination_param = 'task_page' + + def get_prev_marker(self): + return str(int(self.page) - 1) if self.data else '' + + def get_marker(self): + return str(int(self.page) + 1) if self.data else '' + + def get_object_display(self, obj): + return obj.uuid + + def get_object_id(self, obj): + return obj.uuid + + +class AcknowlegedNotificationTable(NotificationTable): + + class Meta(object): + name = 'acknowleged_table' + verbose_name = _('Acknowleged Notifications') + prev_pagination_param = pagination_param = 'acknowledged_page' + table_actions = () diff --git a/adjutant_ui/content/notifications/tabs.py b/adjutant_ui/content/notifications/tabs.py new file mode 100644 index 0000000..7bf18c8 --- /dev/null +++ b/adjutant_ui/content/notifications/tabs.py @@ -0,0 +1,99 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import tabs + +from adjutant_ui.content.notifications import tables as notification_tables +from adjutant_ui.api import adjutant +from adjutant_ui.api.adjutant import AdjutantApiError + + +class UnacknowledgedNotificationsTab(tabs.TableTab): + table_classes = (notification_tables.NotificationTable,) + template_name = 'horizon/common/_detail_table.html' + page_title = _("Unacknowledged Notifications") + name = _('Unacknowledged') + slug = 'unacknowledged' + filters = {'acknowledged': {'exact': False}} + _prev = False + _more = False + _page = 1 + + def get_notification_table_data(self): + notifications = [] + self._page = self.request.GET.get( + self.table_classes[0]._meta.pagination_param, 1) + try: + notifications, self._prev, self._more = adjutant.notification_list( + self.request, filters=self.filters, page=self._page) + except AdjutantApiError as e: + if e.message != "Empty Page": + raise + try: + self._page = 1 + notifications, self._prev, self._more = \ + adjutant.notification_list( + self.request, filters=self.filters, + page=self._page) + except Exception: + exceptions.handle( + self.request, _('Failed to list notifications.')) + except Exception: + exceptions.handle(self.request, _('Failed to list notifications.')) + return notifications + + def has_prev_data(self, table): + table.page = self._page + return self._prev + + def has_more_data(self, table): + table.page = self._page + return self._more + + +class AcknowlededNotificationsTab(UnacknowledgedNotificationsTab): + table_classes = (notification_tables.AcknowlegedNotificationTable,) + page_title = _("Acknowleged Notifications") + name = _('Acknowleged') + slug = 'acknowledged' + filters = {'acknowledged': {'exact': True}} + + def get_acknowleged_table_data(self): + notifications = [] + self._page = self.request.GET.get( + self.table_classes[0]._meta.pagination_param, 1) + try: + notifications, self._prev, self._more = adjutant.notification_list( + self.request, filters=self.filters, page=self._page) + except Exception: + exceptions.handle(self.request, _('Failed to list notifications.')) + return notifications + + +class NotificationTabGroup(tabs.TabGroup): + slug = "notifications" + tabs = (UnacknowledgedNotificationsTab, AcknowlededNotificationsTab, ) + sticky = True + + def get_selected_tab(self): + super(NotificationTabGroup, self).get_selected_tab() + if not self._selected: + for tab in self.tabs: + param = tab.table_classes[0]._meta.pagination_param + if self.request.GET.get(param): + self._selected = self.get_tab(tab.slug) + return self._selected diff --git a/adjutant_ui/content/notifications/templates/notifications/detail.html b/adjutant_ui/content/notifications/templates/notifications/detail.html new file mode 100644 index 0000000..e990f97 --- /dev/null +++ b/adjutant_ui/content/notifications/templates/notifications/detail.html @@ -0,0 +1,32 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load task_filters %} +{% block title %}{% trans "Notification Details" %}{% endblock %} + +{% block page_header %} + {% include 'horizon/common/_detail_header.html' %} +{% endblock %} + +{% block main %} +