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:
lin-hua-cheng 2015-12-03 12:34:01 -08:00
parent d4c7b8f813
commit 0e025d9d71
10 changed files with 262 additions and 0 deletions

View File

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

View File

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

View File

@ -0,0 +1 @@
{"level": "info", "message": "info message"}

View File

@ -0,0 +1 @@
invalid msg file

View File

@ -0,0 +1 @@
{"level": "warning", "message": "warning message"}

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

View File

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

View File

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

View File

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