Merge "Plugins may now register their own user preferences"
This commit is contained in:
commit
77e9949eeb
@ -38,6 +38,8 @@ console_scripts =
|
|||||||
storyboard-migrate = storyboard.migrate.cli:main
|
storyboard-migrate = storyboard.migrate.cli:main
|
||||||
storyboard.worker.task =
|
storyboard.worker.task =
|
||||||
subscription = storyboard.worker.task.subscription:Subscription
|
subscription = storyboard.worker.task.subscription:Subscription
|
||||||
|
storyboard.plugin.user_preferences =
|
||||||
|
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
source-dir = doc/source
|
source-dir = doc/source
|
||||||
|
@ -30,6 +30,7 @@ from storyboard.api.v1.search import search_engine
|
|||||||
from storyboard.notifications.notification_hook import NotificationHook
|
from storyboard.notifications.notification_hook import NotificationHook
|
||||||
from storyboard.openstack.common.gettextutils import _ # noqa
|
from storyboard.openstack.common.gettextutils import _ # noqa
|
||||||
from storyboard.openstack.common import log
|
from storyboard.openstack.common import log
|
||||||
|
from storyboard.plugin.user_preferences import initialize_user_preferences
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
@ -91,6 +92,9 @@ def setup_app(pecan_config=None):
|
|||||||
search_engine_cls = search_engine_impls.ENGINE_IMPLS[search_engine_name]
|
search_engine_cls = search_engine_impls.ENGINE_IMPLS[search_engine_name]
|
||||||
search_engine.set_engine(search_engine_cls())
|
search_engine.set_engine(search_engine_cls())
|
||||||
|
|
||||||
|
# Load user preference plugins
|
||||||
|
initialize_user_preferences()
|
||||||
|
|
||||||
# Setup notifier
|
# Setup notifier
|
||||||
if CONF.enable_notifications:
|
if CONF.enable_notifications:
|
||||||
hooks.append(NotificationHook())
|
hooks.append(NotificationHook())
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
from pecan import abort
|
from pecan import abort
|
||||||
from pecan import request
|
from pecan import request
|
||||||
from pecan import rest
|
from pecan import rest
|
||||||
@ -22,6 +23,11 @@ import wsmeext.pecan as wsme_pecan
|
|||||||
|
|
||||||
from storyboard.api.auth import authorization_checks as checks
|
from storyboard.api.auth import authorization_checks as checks
|
||||||
import storyboard.db.api.users as user_api
|
import storyboard.db.api.users as user_api
|
||||||
|
from storyboard.openstack.common import log
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class UserPreferencesController(rest.RestController):
|
class UserPreferencesController(rest.RestController):
|
||||||
@ -40,7 +46,11 @@ class UserPreferencesController(rest.RestController):
|
|||||||
@wsme_pecan.wsexpose(types.DictType(unicode, unicode), int,
|
@wsme_pecan.wsexpose(types.DictType(unicode, unicode), int,
|
||||||
body=types.DictType(unicode, unicode))
|
body=types.DictType(unicode, unicode))
|
||||||
def post(self, user_id, body):
|
def post(self, user_id, body):
|
||||||
"""Allow a user to update their preferences.
|
"""Allow a user to update their preferences. Note that a user must
|
||||||
|
explicitly set a preference value to Null/None to have it deleted.
|
||||||
|
|
||||||
|
:param user_id The ID of the user whose preferences we're updating.
|
||||||
|
:param body A dictionary of preference values.
|
||||||
"""
|
"""
|
||||||
if request.current_user_id != user_id:
|
if request.current_user_id != user_id:
|
||||||
abort(403)
|
abort(403)
|
||||||
|
@ -18,6 +18,7 @@ from oslo.db import exception as db_exc
|
|||||||
from storyboard.common import exception as exc
|
from storyboard.common import exception as exc
|
||||||
from storyboard.db.api import base as api_base
|
from storyboard.db.api import base as api_base
|
||||||
from storyboard.db import models
|
from storyboard.db import models
|
||||||
|
from storyboard.plugin.user_preferences import PREFERENCE_DEFAULTS
|
||||||
|
|
||||||
|
|
||||||
def user_get(user_id, filter_non_public=False):
|
def user_get(user_id, filter_non_public=False):
|
||||||
@ -74,6 +75,11 @@ def user_get_preferences(user_id):
|
|||||||
for pref in preferences:
|
for pref in preferences:
|
||||||
pref_dict[pref.key] = pref.cast_value
|
pref_dict[pref.key] = pref.cast_value
|
||||||
|
|
||||||
|
# Decorate with plugin defaults.
|
||||||
|
for key in PREFERENCE_DEFAULTS:
|
||||||
|
if key not in pref_dict:
|
||||||
|
pref_dict[key] = PREFERENCE_DEFAULTS[key]
|
||||||
|
|
||||||
return pref_dict
|
return pref_dict
|
||||||
|
|
||||||
|
|
||||||
|
0
storyboard/plugin/__init__.py
Normal file
0
storyboard/plugin/__init__.py
Normal file
67
storyboard/plugin/base.py
Normal file
67
storyboard/plugin/base.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||||
|
#
|
||||||
|
# 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 abc
|
||||||
|
import six
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
from stevedore.enabled import EnabledExtensionManager
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def is_enabled(ext):
|
||||||
|
"""Check to see whether a plugin should be enabled. Assumes that the
|
||||||
|
plugin extends PluginBase.
|
||||||
|
|
||||||
|
:param ext: The extension instance to check.
|
||||||
|
:return: True if it should be enabled. Otherwise false.
|
||||||
|
"""
|
||||||
|
return ext.obj.enabled()
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class PluginBase(object):
|
||||||
|
"""Base class for all storyboard plugins.
|
||||||
|
|
||||||
|
Every storyboard plugin will be provided an instance of the application
|
||||||
|
configuration, and will then be asked whether it should be enabled. Each
|
||||||
|
plugin should decide, given the configuration and the environment,
|
||||||
|
whether it has the necessary resources to operate properly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def enabled(self):
|
||||||
|
"""A method which indicates whether this plugin is properly
|
||||||
|
configured and should be enabled. If it's ready to go, return True.
|
||||||
|
Otherwise, return False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class StoryboardPluginLoader(EnabledExtensionManager):
|
||||||
|
"""The storyboard plugin loader, a stevedore abstraction that formalizes
|
||||||
|
our plugin contract.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, namespace, on_load_failure_callback=None):
|
||||||
|
super(StoryboardPluginLoader, self) \
|
||||||
|
.__init__(namespace=namespace,
|
||||||
|
check_func=is_enabled,
|
||||||
|
invoke_on_load=True,
|
||||||
|
invoke_args=(CONF,),
|
||||||
|
on_load_failure_callback=on_load_failure_callback)
|
68
storyboard/plugin/user_preferences.py
Normal file
68
storyboard/plugin/user_preferences.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||||
|
#
|
||||||
|
# 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 abc
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from storyboard.openstack.common import log
|
||||||
|
from storyboard.plugin.base import PluginBase
|
||||||
|
from storyboard.plugin.base import StoryboardPluginLoader
|
||||||
|
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
PREFERENCE_DEFAULTS = dict()
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_user_preferences():
|
||||||
|
"""Initialize any plugins that were installed via pip. This will parse
|
||||||
|
out all the default preference values into one dictionary for later
|
||||||
|
use in the API.
|
||||||
|
"""
|
||||||
|
manager = StoryboardPluginLoader(
|
||||||
|
namespace='storyboard.plugin.user_preferences')
|
||||||
|
|
||||||
|
if manager.extensions:
|
||||||
|
manager.map(load_preferences, PREFERENCE_DEFAULTS)
|
||||||
|
|
||||||
|
|
||||||
|
def load_preferences(ext, defaults):
|
||||||
|
"""Load all plugin default preferences into our cache.
|
||||||
|
|
||||||
|
:param ext: The extension that's handling this event.
|
||||||
|
:param defaults: The current dict of default preferences.
|
||||||
|
"""
|
||||||
|
|
||||||
|
plugin_defaults = ext.obj.get_default_preferences()
|
||||||
|
|
||||||
|
for key in plugin_defaults:
|
||||||
|
if key in defaults:
|
||||||
|
# Let's not error out here.
|
||||||
|
LOG.error("Duplicate preference key %s found." % (key,))
|
||||||
|
else:
|
||||||
|
defaults[key] = plugin_defaults[key]
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class UserPreferencesPluginBase(PluginBase):
|
||||||
|
"""Base class for a plugin that provides a set of expected user
|
||||||
|
preferences and their default values. By extending this plugin, you can
|
||||||
|
add preferences for your own storyboard plugins and workers, and have
|
||||||
|
them be manageable via your web client (Your client may need to be
|
||||||
|
customized).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_default_preferences(self):
|
||||||
|
"""Return a dictionary of preferences and their default values."""
|
0
storyboard/tests/plugin/__init__.py
Normal file
0
storyboard/tests/plugin/__init__.py
Normal file
58
storyboard/tests/plugin/test_base.py
Normal file
58
storyboard/tests/plugin/test_base.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||||
|
#
|
||||||
|
# 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 stevedore.extension import Extension
|
||||||
|
|
||||||
|
import storyboard.plugin.base as plugin_base
|
||||||
|
import storyboard.tests.base as base
|
||||||
|
|
||||||
|
|
||||||
|
class TestPluginBase(base.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestPluginBase, self).setUp()
|
||||||
|
|
||||||
|
self.extensions = []
|
||||||
|
self.extensions.append(Extension(
|
||||||
|
'test_one', None, None,
|
||||||
|
TestBasePlugin(dict())
|
||||||
|
))
|
||||||
|
|
||||||
|
def test_extensibility(self):
|
||||||
|
"""Assert that we can actually instantiate a plugin."""
|
||||||
|
|
||||||
|
plugin = TestBasePlugin(dict())
|
||||||
|
self.assertIsNotNone(plugin)
|
||||||
|
self.assertTrue(plugin.enabled())
|
||||||
|
|
||||||
|
def test_plugin_loader(self):
|
||||||
|
manager = plugin_base.StoryboardPluginLoader.make_test_instance(
|
||||||
|
self.extensions,
|
||||||
|
namespace='storyboard.plugin.testing'
|
||||||
|
)
|
||||||
|
|
||||||
|
results = manager.map(self._count_invocations)
|
||||||
|
|
||||||
|
# One must exist.
|
||||||
|
self.assertEqual(1, len(manager.extensions))
|
||||||
|
|
||||||
|
# One should be invoked.
|
||||||
|
self.assertEqual(1, len(results))
|
||||||
|
|
||||||
|
def _count_invocations(self, ext):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestBasePlugin(plugin_base.PluginBase):
|
||||||
|
def enabled(self):
|
||||||
|
return True
|
84
storyboard/tests/plugin/test_user_preferences.py
Normal file
84
storyboard/tests/plugin/test_user_preferences.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||||
|
#
|
||||||
|
# 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 stevedore.extension import Extension
|
||||||
|
|
||||||
|
import storyboard.plugin.base as plugin_base
|
||||||
|
import storyboard.plugin.user_preferences as prefs_base
|
||||||
|
import storyboard.tests.base as base
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserPreferencesPluginBase(base.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestUserPreferencesPluginBase, self).setUp()
|
||||||
|
|
||||||
|
self.extensions = []
|
||||||
|
self.extensions.append(Extension(
|
||||||
|
'test_one', None, None,
|
||||||
|
TestPreferencesPlugin(dict())
|
||||||
|
))
|
||||||
|
self.extensions.append(Extension(
|
||||||
|
'test_two', None, None,
|
||||||
|
TestOtherPreferencesPlugin(dict())
|
||||||
|
))
|
||||||
|
|
||||||
|
def test_extensibility(self):
|
||||||
|
"""Assert that we can actually instantiate a plugin."""
|
||||||
|
|
||||||
|
plugin = TestPreferencesPlugin(dict())
|
||||||
|
self.assertIsNotNone(plugin)
|
||||||
|
self.assertTrue(plugin.enabled())
|
||||||
|
|
||||||
|
def test_plugin_loader(self):
|
||||||
|
"""Perform a single plugin loading run, including two plugins and a
|
||||||
|
couple of overlapping preferences.
|
||||||
|
"""
|
||||||
|
manager = plugin_base.StoryboardPluginLoader.make_test_instance(
|
||||||
|
self.extensions,
|
||||||
|
namespace='storyboard.plugin.user_preferences')
|
||||||
|
|
||||||
|
loaded_prefs = dict()
|
||||||
|
|
||||||
|
self.assertEqual(2, len(manager.extensions))
|
||||||
|
manager.map(prefs_base.load_preferences, loaded_prefs)
|
||||||
|
|
||||||
|
self.assertTrue("foo" in loaded_prefs)
|
||||||
|
self.assertTrue("omg" in loaded_prefs)
|
||||||
|
self.assertTrue("lol" in loaded_prefs)
|
||||||
|
|
||||||
|
self.assertEqual(loaded_prefs["foo"], "baz")
|
||||||
|
self.assertEqual(loaded_prefs["omg"], "wat")
|
||||||
|
self.assertEqual(loaded_prefs["lol"], "cat")
|
||||||
|
|
||||||
|
|
||||||
|
class TestPreferencesPlugin(prefs_base.UserPreferencesPluginBase):
|
||||||
|
def get_default_preferences(self):
|
||||||
|
return {
|
||||||
|
"foo": "baz",
|
||||||
|
"omg": "wat"
|
||||||
|
}
|
||||||
|
|
||||||
|
def enabled(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class TestOtherPreferencesPlugin(prefs_base.UserPreferencesPluginBase):
|
||||||
|
def get_default_preferences(self):
|
||||||
|
return {
|
||||||
|
"foo": "bar",
|
||||||
|
"lol": "cat"
|
||||||
|
}
|
||||||
|
|
||||||
|
def enabled(self):
|
||||||
|
return True
|
Loading…
x
Reference in New Issue
Block a user