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
This commit is contained in:
Amelia Cordwell 2017-07-12 17:04:04 +12:00 committed by Adrian Turjak
parent d6c7521925
commit 0f170a9a32
12 changed files with 464 additions and 4 deletions

View File

@ -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'],

View File

@ -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"),)

View File

@ -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 = ()

View File

@ -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

View File

@ -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 %}
<div class="detail">
<dl class="dl-horizontal">
<dt>{% trans "ID" %}</dt>
<dd>{{ notification.uuid }}</dd>
<dt>{% trans "Error" %}</dt>
<dd>{{ notification.error }}</dd>
<dt>{% trans "Notes" %}</dt>
<dd>{{ notification.notes }}</dd>
<dt>{% trans "Created On" %}</dt>
<dd>{{ notification.created_on }}</dd>
<dt>{% trans "Acknowleged" %}</dt>
<dd>{{ notification.acknowledged }}</dd>
<dt>{% trans "Task" %}</dt>
<dd><a href="{% url 'horizon:management:tasks:detail' notification.task %}">{{ notification.task }}</a></dd>
<dt>{% trans "Task Type" %}</dt>
<dd>{{ task.task_type }}</dd>
<dt>{% trans "Request By" %}</dt>
<dd>{{ task.request_by }}</dd>
<dt>{% trans "Task Status" %}</dt>
<dd>{{ task.status }}</dd>
</div>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Admin Notifications" %}{% endblock %}
{% block main %}
<div class="row">
<div class="col-sm-12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,2 @@
{% extends 'horizon/common/_data_table.html' %}
{% block table_css_classes %}table datatable {% endblock %}

View File

@ -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<notif_id>[^/]+)/$',
views.NotificationDetailView.as_view(), name='detail'),
]

View File

@ -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)

View File

@ -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"),)

View File

@ -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'
]