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.worker.task =
|
||||
subscription = storyboard.worker.task.subscription:Subscription
|
||||
storyboard.plugin.user_preferences =
|
||||
|
||||
|
||||
[build_sphinx]
|
||||
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.openstack.common.gettextutils import _ # noqa
|
||||
from storyboard.openstack.common import log
|
||||
from storyboard.plugin.user_preferences import initialize_user_preferences
|
||||
|
||||
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.set_engine(search_engine_cls())
|
||||
|
||||
# Load user preference plugins
|
||||
initialize_user_preferences()
|
||||
|
||||
# Setup notifier
|
||||
if CONF.enable_notifications:
|
||||
hooks.append(NotificationHook())
|
||||
|
@ -13,6 +13,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from oslo.config import cfg
|
||||
from pecan import abort
|
||||
from pecan import request
|
||||
from pecan import rest
|
||||
@ -22,6 +23,11 @@ import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from storyboard.api.auth import authorization_checks as checks
|
||||
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):
|
||||
@ -40,7 +46,11 @@ class UserPreferencesController(rest.RestController):
|
||||
@wsme_pecan.wsexpose(types.DictType(unicode, unicode), int,
|
||||
body=types.DictType(unicode, unicode))
|
||||
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:
|
||||
abort(403)
|
||||
|
@ -18,6 +18,7 @@ from oslo.db import exception as db_exc
|
||||
from storyboard.common import exception as exc
|
||||
from storyboard.db.api import base as api_base
|
||||
from storyboard.db import models
|
||||
from storyboard.plugin.user_preferences import PREFERENCE_DEFAULTS
|
||||
|
||||
|
||||
def user_get(user_id, filter_non_public=False):
|
||||
@ -74,6 +75,11 @@ def user_get_preferences(user_id):
|
||||
for pref in preferences:
|
||||
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
|
||||
|
||||
|
||||
|
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