From 0f170a9a32c4b7d35ad3afef75ad8aa003f188c8 Mon Sep 17 00:00:00 2001 From: Amelia Cordwell Date: Wed, 12 Jul 2017 17:04:04 +1200 Subject: [PATCH] Add Notification Panel - List view of all notifications (will be split into acknowleged and unacknowledged) - Detail view of each notification - Acknowledge notifications - Error notifications show up in red Change-Id: I0800201b42424bfdd3f27dd090502f28e4f54227 --- adjutant_ui/api/adjutant.py | 84 ++++++++++++- adjutant_ui/content/notifications/__init__.py | 0 adjutant_ui/content/notifications/panel.py | 23 ++++ adjutant_ui/content/notifications/tables.py | 111 ++++++++++++++++++ adjutant_ui/content/notifications/tabs.py | 99 ++++++++++++++++ .../templates/notifications/detail.html | 32 +++++ .../templates/notifications/index.html | 11 ++ .../notifications/table_override.html | 2 + adjutant_ui/content/notifications/urls.py | 25 ++++ adjutant_ui/content/notifications/views.py | 66 +++++++++++ adjutant_ui/content/tasks/panel.py | 2 +- .../_6100_management_notification_list.py | 13 ++ 12 files changed, 464 insertions(+), 4 deletions(-) create mode 100644 adjutant_ui/content/notifications/__init__.py create mode 100644 adjutant_ui/content/notifications/panel.py create mode 100644 adjutant_ui/content/notifications/tables.py create mode 100644 adjutant_ui/content/notifications/tabs.py create mode 100644 adjutant_ui/content/notifications/templates/notifications/detail.html create mode 100644 adjutant_ui/content/notifications/templates/notifications/index.html create mode 100644 adjutant_ui/content/notifications/templates/notifications/table_override.html create mode 100644 adjutant_ui/content/notifications/urls.py create mode 100644 adjutant_ui/content/notifications/views.py create mode 100644 adjutant_ui/enabled/_6100_management_notification_list.py 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 %} +
+
+
{% trans "ID" %}
+
{{ notification.uuid }}
+
{% trans "Error" %}
+
{{ notification.error }}
+
{% trans "Notes" %}
+
{{ notification.notes }}
+
{% trans "Created On" %}
+
{{ notification.created_on }}
+
{% trans "Acknowleged" %}
+
{{ notification.acknowledged }}
+
{% trans "Task" %}
+
{{ notification.task }}
+
{% trans "Task Type" %}
+
{{ task.task_type }}
+
{% trans "Request By" %}
+
{{ task.request_by }}
+
{% trans "Task Status" %}
+
{{ task.status }}
+
+{% endblock %} diff --git a/adjutant_ui/content/notifications/templates/notifications/index.html b/adjutant_ui/content/notifications/templates/notifications/index.html new file mode 100644 index 0000000..81ec3ed --- /dev/null +++ b/adjutant_ui/content/notifications/templates/notifications/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Admin Notifications" %}{% endblock %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} diff --git a/adjutant_ui/content/notifications/templates/notifications/table_override.html b/adjutant_ui/content/notifications/templates/notifications/table_override.html new file mode 100644 index 0000000..9b0412f --- /dev/null +++ b/adjutant_ui/content/notifications/templates/notifications/table_override.html @@ -0,0 +1,2 @@ +{% extends 'horizon/common/_data_table.html' %} +{% block table_css_classes %}table datatable {% endblock %} diff --git a/adjutant_ui/content/notifications/urls.py b/adjutant_ui/content/notifications/urls.py new file mode 100644 index 0000000..d288980 --- /dev/null +++ b/adjutant_ui/content/notifications/urls.py @@ -0,0 +1,25 @@ +# 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.conf.urls import url + +from adjutant_ui.content.notifications import views + + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^(?P[^/]+)/$', + views.NotificationDetailView.as_view(), name='detail'), +] diff --git a/adjutant_ui/content/notifications/views.py b/adjutant_ui/content/notifications/views.py new file mode 100644 index 0000000..fd91894 --- /dev/null +++ b/adjutant_ui/content/notifications/views.py @@ -0,0 +1,66 @@ +# 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.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import views +from horizon import tabs + +from horizon.utils import memoized + +from adjutant_ui.content.notifications import tabs as notification_tab +from adjutant_ui.content.notifications import tables as notification_tables +from adjutant_ui.api import adjutant + + +class IndexView(tabs.TabbedTableView): + tab_group_class = notification_tab.NotificationTabGroup + template_name = 'notifications/index.html' + redirect_url = 'horizon:management:notifications:index' + page_title = _("Admin Notifications") + + +class NotificationDetailView(views.HorizonTemplateView): + redirect_url = "horizon:management:notifications:index" + template_name = 'notifications/detail.html' + page_title = "Notification: {{ notification.uuid }}" + + def get_context_data(self, **kwargs): + context = super(NotificationDetailView, self).get_context_data( + **kwargs) + notification, task = self.get_data() + context["notification"] = notification + context['task'] = task + context["url"] = reverse(self.redirect_url) + context["actions"] = self._get_actions(notification=notification) + return context + + def _get_actions(self, notification): + table = notification_tables.NotificationTable(self.request) + return table.render_row_actions(notification) + + @memoized.memoized_method + def get_data(self): + try: + notification = adjutant.notification_obj_get( + self.request, self.kwargs['notif_id']) + task = adjutant.task_obj_get(self.request, + task_id=notification.task) + return notification, task + except Exception: + msg = _('Unable to retrieve notification.') + url = reverse('horizon:management:notifications:index') + exceptions.handle(self.request, msg, redirect=url) diff --git a/adjutant_ui/content/tasks/panel.py b/adjutant_ui/content/tasks/panel.py index 499cfd6..c6b786e 100644 --- a/adjutant_ui/content/tasks/panel.py +++ b/adjutant_ui/content/tasks/panel.py @@ -20,4 +20,4 @@ import horizon class TaskList(horizon.Panel): name = _('Admin Tasks') slug = 'tasks' - policy_rules = (("identity", "role:admin"),) + policy_rules = (("identity", "admin_required"),) diff --git a/adjutant_ui/enabled/_6100_management_notification_list.py b/adjutant_ui/enabled/_6100_management_notification_list.py new file mode 100644 index 0000000..427f10b --- /dev/null +++ b/adjutant_ui/enabled/_6100_management_notification_list.py @@ -0,0 +1,13 @@ +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'notifications' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'management' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'default' + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'adjutant_ui.content.notifications.panel.NotificationPanel' + +ADD_INSTALLED_APPS = [ + 'adjutant_ui.content.notifications' +]