Add message of the day option
Allow user to configure messages to display to the user after they login. Change-Id: I6dc0318708d0f964e52c8b127718297fc723651c Implements: blueprint message-of-the-day
This commit is contained in:
parent
d4c7b8f813
commit
0e025d9d71
@ -508,6 +508,28 @@ This setting can be used in the case where a separate panel is used for
|
|||||||
managing a custom property or if a certain custom property should never be
|
managing a custom property or if a certain custom property should never be
|
||||||
edited.
|
edited.
|
||||||
|
|
||||||
|
|
||||||
|
``MESSAGES_PATH``
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. versionadded:: 9.0.0(Mitaka)
|
||||||
|
|
||||||
|
Default: ``None``
|
||||||
|
|
||||||
|
The absolute path to the directory where message files are collected.
|
||||||
|
|
||||||
|
When the user logins to horizon, the message files collected are processed
|
||||||
|
and displayed to the user. Each message file should contain a JSON formatted
|
||||||
|
data and must have a .json file extension. For example::
|
||||||
|
|
||||||
|
{
|
||||||
|
"level": "info",
|
||||||
|
"message": "message of the day here"
|
||||||
|
}
|
||||||
|
|
||||||
|
Possible values for level are: success, info, warning and error.
|
||||||
|
|
||||||
|
|
||||||
``OPENSTACK_API_VERSIONS``
|
``OPENSTACK_API_VERSIONS``
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
|
@ -217,6 +217,11 @@ class WorkflowValidationError(HorizonException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MessageFailure(HorizonException):
|
||||||
|
"""Exception raised during message notification."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class HandledException(HorizonException):
|
class HandledException(HorizonException):
|
||||||
"""Used internally to track exceptions that have gone through
|
"""Used internally to track exceptions that have gone through
|
||||||
:func:`horizon.exceptions.handle` more than once.
|
:func:`horizon.exceptions.handle` more than once.
|
||||||
|
150
horizon/notifications.py
Normal file
150
horizon/notifications.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from horizon import exceptions
|
||||||
|
from horizon import messages
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_MESSAGES_CACHE = None
|
||||||
|
_MESSAGES_MTIME = None
|
||||||
|
|
||||||
|
|
||||||
|
class JSONMessage(object):
|
||||||
|
|
||||||
|
INFO = messages.info
|
||||||
|
SUCCESS = messages.success
|
||||||
|
WARNING = messages.warning
|
||||||
|
ERROR = messages.error
|
||||||
|
|
||||||
|
MESSAGE_LEVELS = {
|
||||||
|
'info': INFO,
|
||||||
|
'success': SUCCESS,
|
||||||
|
'warning': WARNING,
|
||||||
|
'error': ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, path, fail_silently=False):
|
||||||
|
self._path = path
|
||||||
|
self._data = ''
|
||||||
|
|
||||||
|
self.failed = False
|
||||||
|
self.fail_silently = fail_silently
|
||||||
|
self.message = ''
|
||||||
|
self.level = self.INFO
|
||||||
|
self.level_name = 'info'
|
||||||
|
|
||||||
|
def _read(self):
|
||||||
|
with open(self._path, 'rb') as file_obj:
|
||||||
|
self._data = file_obj.read()
|
||||||
|
|
||||||
|
def _parse(self):
|
||||||
|
attrs = {}
|
||||||
|
try:
|
||||||
|
data = self._data.decode('utf-8')
|
||||||
|
attrs = json.loads(data)
|
||||||
|
except ValueError as exc:
|
||||||
|
self.failed = True
|
||||||
|
|
||||||
|
msg = _("Message json file '%(path)s' is malformed."
|
||||||
|
" %(exception)s")
|
||||||
|
msg = msg % {'path': self._path, 'exception': str(exc)}
|
||||||
|
if self.fail_silently:
|
||||||
|
LOG.warning(msg)
|
||||||
|
else:
|
||||||
|
raise exceptions.MessageFailure(msg)
|
||||||
|
else:
|
||||||
|
level_name = attrs.get('level', 'info')
|
||||||
|
if level_name in self.MESSAGE_LEVELS:
|
||||||
|
self.level_name = level_name
|
||||||
|
|
||||||
|
self.level = self.MESSAGE_LEVELS.get(self.level_name, self.INFO)
|
||||||
|
self.message = attrs.get('message', '')
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
"""Read and parse the message file."""
|
||||||
|
try:
|
||||||
|
self._read()
|
||||||
|
self._parse()
|
||||||
|
except Exception as exc:
|
||||||
|
self.failed = True
|
||||||
|
|
||||||
|
msg = _("Error processing message json file '%(path)s': "
|
||||||
|
"%(exception)s")
|
||||||
|
msg = msg % {'path': self._path, 'exception': str(exc)}
|
||||||
|
if self.fail_silently:
|
||||||
|
LOG.warning(msg)
|
||||||
|
else:
|
||||||
|
raise exceptions.MessageFailure(msg)
|
||||||
|
|
||||||
|
def send_message(self, request):
|
||||||
|
if self.failed:
|
||||||
|
return
|
||||||
|
self.level(request, mark_safe(self.message))
|
||||||
|
|
||||||
|
|
||||||
|
def _is_path(path):
|
||||||
|
if os.path.exists(path) and os.path.isdir(path):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _get_processed_messages(messages_path):
|
||||||
|
msgs = list()
|
||||||
|
|
||||||
|
if not _is_path(messages_path):
|
||||||
|
LOG.error('%s is not a valid messages path.', messages_path)
|
||||||
|
return msgs
|
||||||
|
|
||||||
|
# Get all files from messages_path with .json extension
|
||||||
|
for fname in glob.glob(os.path.join(messages_path, '*.json')):
|
||||||
|
fpath = os.path.join(messages_path, fname)
|
||||||
|
|
||||||
|
msg = JSONMessage(fpath, fail_silently=True)
|
||||||
|
msg.load()
|
||||||
|
|
||||||
|
if not msg.failed:
|
||||||
|
msgs.append(msg)
|
||||||
|
|
||||||
|
return msgs
|
||||||
|
|
||||||
|
|
||||||
|
def process_message_notification(request, messages_path):
|
||||||
|
"""Process all the msg file found in the message directory"""
|
||||||
|
if not messages_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
global _MESSAGES_CACHE
|
||||||
|
global _MESSAGES_MTIME
|
||||||
|
|
||||||
|
# NOTE (lhcheng): Cache the processed messages to avoid parsing
|
||||||
|
# the files every time. Check directory modification time if
|
||||||
|
# reload is necessary.
|
||||||
|
if (_MESSAGES_CACHE is None
|
||||||
|
or _MESSAGES_MTIME != os.path.getmtime(messages_path)):
|
||||||
|
_MESSAGES_CACHE = _get_processed_messages(messages_path)
|
||||||
|
_MESSAGES_MTIME = os.path.getmtime(messages_path)
|
||||||
|
|
||||||
|
for msg in _MESSAGES_CACHE:
|
||||||
|
msg.send_message(request)
|
1
horizon/test/messages/test_info.json
Normal file
1
horizon/test/messages/test_info.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"level": "info", "message": "info message"}
|
1
horizon/test/messages/test_invalid.json
Normal file
1
horizon/test/messages/test_invalid.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
invalid msg file
|
1
horizon/test/messages/test_warning.json
Normal file
1
horizon/test/messages/test_warning.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"level": "warning", "message": "warning message"}
|
59
horizon/test/tests/notifications.py
Normal file
59
horizon/test/tests/notifications.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from horizon import exceptions
|
||||||
|
from horizon.notifications import JSONMessage
|
||||||
|
from horizon.test import helpers as test
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationTests(test.TestCase):
|
||||||
|
|
||||||
|
MESSAGES_PATH = os.path.abspath(os.path.join(settings.ROOT_PATH,
|
||||||
|
'messages'))
|
||||||
|
|
||||||
|
def _test_msg(self, path, expected_level, expected_msg=''):
|
||||||
|
msg = JSONMessage(path)
|
||||||
|
msg.load()
|
||||||
|
|
||||||
|
self.assertEqual(expected_level, msg.level_name)
|
||||||
|
self.assertEqual(expected_msg, msg.message)
|
||||||
|
|
||||||
|
def test_warning_msg(self):
|
||||||
|
path = self.MESSAGES_PATH + '/test_warning.json'
|
||||||
|
|
||||||
|
self._test_msg(path, 'warning', 'warning message')
|
||||||
|
|
||||||
|
def test_info_msg(self):
|
||||||
|
path = self.MESSAGES_PATH + '/test_info.json'
|
||||||
|
|
||||||
|
self._test_msg(path, 'info', 'info message')
|
||||||
|
|
||||||
|
def test_invalid_msg_file(self):
|
||||||
|
path = self.MESSAGES_PATH + '/test_invalid.json'
|
||||||
|
|
||||||
|
with self.assertRaises(exceptions.MessageFailure):
|
||||||
|
msg = JSONMessage(path)
|
||||||
|
msg.load()
|
||||||
|
|
||||||
|
def test_invalid_msg_file_fail_silently(self):
|
||||||
|
path = self.MESSAGES_PATH + '/test_invalid.json'
|
||||||
|
|
||||||
|
msg = JSONMessage(path, fail_silently=True)
|
||||||
|
msg.load()
|
||||||
|
|
||||||
|
self.assertTrue(msg.failed)
|
@ -40,6 +40,11 @@ WEBROOT = '/'
|
|||||||
#CSRF_COOKIE_SECURE = True
|
#CSRF_COOKIE_SECURE = True
|
||||||
#SESSION_COOKIE_SECURE = True
|
#SESSION_COOKIE_SECURE = True
|
||||||
|
|
||||||
|
# The absolute path to the directory where message files are collected.
|
||||||
|
# The message file must have a .json file extension. When the user logins to
|
||||||
|
# horizon, the message files collected are processed and displayed to the user.
|
||||||
|
#MESSAGES_PATH=None
|
||||||
|
|
||||||
# Overrides for OpenStack API versions. Use this setting to force the
|
# Overrides for OpenStack API versions. Use this setting to force the
|
||||||
# OpenStack dashboard to use a specific API version for a given service API.
|
# OpenStack dashboard to use a specific API version for a given service API.
|
||||||
# Versions specified here should be integers or floats, not strings.
|
# Versions specified here should be integers or floats, not strings.
|
||||||
|
@ -12,12 +12,17 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django import shortcuts
|
from django import shortcuts
|
||||||
import django.views.decorators.vary
|
import django.views.decorators.vary
|
||||||
|
|
||||||
import horizon
|
import horizon
|
||||||
from horizon import base
|
from horizon import base
|
||||||
from horizon import exceptions
|
from horizon import exceptions
|
||||||
|
from horizon import notifications
|
||||||
|
|
||||||
|
|
||||||
|
MESSAGES_PATH = getattr(settings, 'MESSAGES_PATH', None)
|
||||||
|
|
||||||
|
|
||||||
def get_user_home(user):
|
def get_user_home(user):
|
||||||
@ -42,4 +47,8 @@ def splash(request):
|
|||||||
response = shortcuts.redirect(horizon.get_user_home(request.user))
|
response = shortcuts.redirect(horizon.get_user_home(request.user))
|
||||||
if 'logout_reason' in request.COOKIES:
|
if 'logout_reason' in request.COOKIES:
|
||||||
response.delete_cookie('logout_reason')
|
response.delete_cookie('logout_reason')
|
||||||
|
# Display Message of the Day message from the message files
|
||||||
|
# located in MESSAGES_PATH
|
||||||
|
if MESSAGES_PATH:
|
||||||
|
notifications.process_message_notification(request, MESSAGES_PATH)
|
||||||
return response
|
return response
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- >
|
||||||
|
[`blueprint message-of-the-day <https://blueprints.launchpad.net/horizon/+spec/message-of-the-day>`_]
|
||||||
|
Message of the day can now be configured in horizon, this will be displayed
|
||||||
|
to the user whenever they login. To enable the feature set ``MESSAGES_PATH``
|
||||||
|
in the local_settting.py to the directory where message files are located.
|
||||||
|
The message file must have a .json file extension.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user