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
|
||||
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``
|
||||
--------------------------
|
||||
|
||||
|
@ -217,6 +217,11 @@ class WorkflowValidationError(HorizonException):
|
||||
pass
|
||||
|
||||
|
||||
class MessageFailure(HorizonException):
|
||||
"""Exception raised during message notification."""
|
||||
pass
|
||||
|
||||
|
||||
class HandledException(HorizonException):
|
||||
"""Used internally to track exceptions that have gone through
|
||||
: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
|
||||
#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
|
||||
# OpenStack dashboard to use a specific API version for a given service API.
|
||||
# Versions specified here should be integers or floats, not strings.
|
||||
|
@ -12,12 +12,17 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import settings
|
||||
from django import shortcuts
|
||||
import django.views.decorators.vary
|
||||
|
||||
import horizon
|
||||
from horizon import base
|
||||
from horizon import exceptions
|
||||
from horizon import notifications
|
||||
|
||||
|
||||
MESSAGES_PATH = getattr(settings, 'MESSAGES_PATH', None)
|
||||
|
||||
|
||||
def get_user_home(user):
|
||||
@ -42,4 +47,8 @@ def splash(request):
|
||||
response = shortcuts.redirect(horizon.get_user_home(request.user))
|
||||
if 'logout_reason' in request.COOKIES:
|
||||
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
|
||||
|
@ -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