Workflow: Make steps pluggable via horizon plugin config
This commit enhances django workflow implementation to allow horizon plugins to add workflow steps to a workflow in other repository like the main horizon repo. New setting "EXTRA_STEPS" is introduced to the horizon plugin 'enabled' file. To this aim, the workflow class looks up HORIZON_CONFIG['extra_steps'] with its class full name and loads them as extra steps if any. HORIZON_CONFIG['extra_steps'] are populated via horizon plugin settings. This commit completes the blueprint. blueprint horizon-plugin-tab-for-info-and-quotas Change-Id: I347d113f47587932e4f583d3152e781ad1a4849f
This commit is contained in:
parent
07237c1fc6
commit
e50a69d69f
@ -130,6 +130,36 @@ listed there will be appended to the auto-discovered files.
|
||||
|
||||
If set to ``True``, this settings file will not be added to the settings.
|
||||
|
||||
``EXTRA_STEPS``
|
||||
---------------
|
||||
|
||||
.. versionadded:: 14.0.0(Rocky)
|
||||
|
||||
Extra workflow steps can be added to a workflow in horizon or other
|
||||
horizon plugins by using this setting. Extra steps will be shown after
|
||||
default steps defined in a corresponding workflow.
|
||||
|
||||
This is a dict setting. A key of the dict specifies a workflow which extra
|
||||
step(s) are added. The key must match a full class name of the target workflow.
|
||||
|
||||
A value of the dict is a list of full name of an extra step classes (where a
|
||||
module name and a class name must be delimiteed by a period). Steps specified
|
||||
via ``EXTRA_STEPS`` will be displayed in the order of being registered.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
EXTRA_STEPS = {
|
||||
'openstack_dashboard.dashboards.identity.projects.workflows.UpdateQuota':
|
||||
(
|
||||
('openstack_dashboard.dashboards.identity.projects.workflows.'
|
||||
'UpdateVolumeQuota'),
|
||||
('openstack_dashboard.dashboards.identity.projects.workflows.'
|
||||
'UpdateNetworkQuota'),
|
||||
),
|
||||
}
|
||||
|
||||
``EXTRA_TABS``
|
||||
--------------
|
||||
|
||||
|
@ -73,6 +73,14 @@ class TestActionThree(workflows.Action):
|
||||
slug = "test_action_three"
|
||||
|
||||
|
||||
class TestActionFour(workflows.Action):
|
||||
field_four = forms.CharField(widget=forms.widgets.Textarea)
|
||||
|
||||
class Meta(object):
|
||||
name = "Test Action Four"
|
||||
slug = "test_action_four"
|
||||
|
||||
|
||||
class AdminAction(workflows.Action):
|
||||
admin_id = forms.CharField(label="Admin")
|
||||
|
||||
@ -114,7 +122,7 @@ class TestStepTwo(workflows.Step):
|
||||
"other_callback_func")}
|
||||
|
||||
|
||||
class TestExtraStep(workflows.Step):
|
||||
class TestStepThree(workflows.Step):
|
||||
action_class = TestActionThree
|
||||
depends_on = ("project_id",)
|
||||
contributes = ("extra_data",)
|
||||
@ -123,6 +131,11 @@ class TestExtraStep(workflows.Step):
|
||||
before = TestStepTwo
|
||||
|
||||
|
||||
class TestStepFour(workflows.Step):
|
||||
action_class = TestActionFour
|
||||
contributes = ("field_four",)
|
||||
|
||||
|
||||
class AdminStep(workflows.Step):
|
||||
action_class = AdminAction
|
||||
contributes = ("admin_id",)
|
||||
@ -147,6 +160,11 @@ class TestWorkflow(workflows.Workflow):
|
||||
default_steps = (TestStepOne, TestStepTwo)
|
||||
|
||||
|
||||
class TestWorkflowWithConfig(workflows.Workflow):
|
||||
slug = "test_workflow"
|
||||
default_steps = (TestStepOne,)
|
||||
|
||||
|
||||
class TestWorkflowView(workflows.WorkflowView):
|
||||
workflow_class = TestWorkflow
|
||||
template_name = "workflow.html"
|
||||
@ -176,17 +194,35 @@ class WorkflowsTests(test.TestCase):
|
||||
self._reset_workflow()
|
||||
|
||||
def _reset_workflow(self):
|
||||
TestWorkflow._cls_registry = set([])
|
||||
TestWorkflow._cls_registry = []
|
||||
|
||||
def test_workflow_construction(self):
|
||||
TestWorkflow.register(TestExtraStep)
|
||||
TestWorkflow.register(TestStepThree)
|
||||
flow = TestWorkflow(self.request)
|
||||
self.assertQuerysetEqual(flow.steps,
|
||||
['<TestStepOne: test_action_one>',
|
||||
'<TestExtraStep: test_action_three>',
|
||||
'<TestStepThree: test_action_three>',
|
||||
'<TestStepTwo: test_action_two>'])
|
||||
self.assertEqual(set(['project_id']), flow.depends_on)
|
||||
|
||||
@test.update_settings(HORIZON_CONFIG={'extra_steps': {
|
||||
'horizon.test.unit.workflows.test_workflows.TestWorkflowWithConfig': (
|
||||
'horizon.test.unit.workflows.test_workflows.TestStepTwo',
|
||||
'horizon.test.unit.workflows.test_workflows.TestStepThree',
|
||||
'horizon.test.unit.workflows.test_workflows.TestStepFour',
|
||||
),
|
||||
}})
|
||||
def test_workflow_construction_with_config(self):
|
||||
flow = TestWorkflowWithConfig(self.request)
|
||||
# NOTE: TestStepThree must be placed between TestStepOne and
|
||||
# TestStepTwo in honor of before/after of TestStepThree.
|
||||
self.assertQuerysetEqual(flow.steps,
|
||||
['<TestStepOne: test_action_one>',
|
||||
'<TestStepThree: test_action_three>',
|
||||
'<TestStepTwo: test_action_two>',
|
||||
'<TestStepFour: test_action_four>',
|
||||
])
|
||||
|
||||
def test_step_construction(self):
|
||||
step_one = TestStepOne(TestWorkflow(self.request))
|
||||
# Action slug is moved from Meta by metaclass, and
|
||||
@ -236,7 +272,7 @@ class WorkflowsTests(test.TestCase):
|
||||
InvalidStep(TestWorkflow(self.request))
|
||||
|
||||
def test_connection_handlers_called(self):
|
||||
TestWorkflow.register(TestExtraStep)
|
||||
TestWorkflow.register(TestStepThree)
|
||||
flow = TestWorkflow(self.request)
|
||||
|
||||
# This should set the value without any errors, but trigger nothing
|
||||
@ -292,15 +328,15 @@ class WorkflowsTests(test.TestCase):
|
||||
['<TestStepOne: test_action_one>',
|
||||
'<TestStepTwo: test_action_two>'])
|
||||
|
||||
TestWorkflow.register(TestExtraStep)
|
||||
TestWorkflow.register(TestStepThree)
|
||||
flow = TestWorkflow(req)
|
||||
self.assertQuerysetEqual(flow.steps,
|
||||
['<TestStepOne: test_action_one>',
|
||||
'<TestExtraStep: test_action_three>',
|
||||
'<TestStepThree: test_action_three>',
|
||||
'<TestStepTwo: test_action_two>'])
|
||||
|
||||
def test_workflow_render(self):
|
||||
TestWorkflow.register(TestExtraStep)
|
||||
TestWorkflow.register(TestStepThree)
|
||||
req = self.factory.get("/foo")
|
||||
flow = TestWorkflow(req)
|
||||
output = http.HttpResponse(flow.render())
|
||||
|
@ -12,11 +12,13 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
import copy
|
||||
from importlib import import_module
|
||||
import inspect
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django import forms
|
||||
from django.forms.forms import NON_FIELD_ERRORS
|
||||
from django import template
|
||||
@ -25,6 +27,7 @@ from django.template.defaultfilters import safe
|
||||
from django.template.defaultfilters import slugify
|
||||
from django import urls
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils import module_loading
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from openstack_auth import policy
|
||||
import six
|
||||
@ -481,7 +484,7 @@ class Step(object):
|
||||
class WorkflowMetaclass(type):
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
super(WorkflowMetaclass, mcs).__new__(mcs, name, bases, attrs)
|
||||
attrs["_cls_registry"] = set([])
|
||||
attrs["_cls_registry"] = []
|
||||
return type.__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
@ -652,10 +655,13 @@ class Workflow(html.HTMLElement):
|
||||
self.entry_point = entry_point
|
||||
self.object = None
|
||||
|
||||
self._register_steps_from_config()
|
||||
|
||||
# Put together our steps in order. Note that we pre-register
|
||||
# non-default steps so that we can identify them and subsequently
|
||||
# insert them in order correctly.
|
||||
self._registry = dict([(step_class, step_class(self)) for step_class
|
||||
self._registry = collections.OrderedDict(
|
||||
[(step_class, step_class(self)) for step_class
|
||||
in self.__class__._cls_registry
|
||||
if step_class not in self.default_steps])
|
||||
self._gather_steps()
|
||||
@ -698,6 +704,27 @@ class Workflow(html.HTMLElement):
|
||||
if step.slug == slug:
|
||||
return step
|
||||
|
||||
def _register_steps_from_config(self):
|
||||
my_name = '.'.join([self.__class__.__module__,
|
||||
self.__class__.__name__])
|
||||
horizon_config = settings.HORIZON_CONFIG.get('extra_steps', {})
|
||||
extra_steps = horizon_config.get(my_name, [])
|
||||
for step in extra_steps:
|
||||
self._register_step_from_config(step, my_name)
|
||||
|
||||
def _register_step_from_config(self, step_config, my_name):
|
||||
if not isinstance(step_config, str):
|
||||
LOG.error('Extra step definition must be a string '
|
||||
'(workflow "%s"', my_name)
|
||||
return
|
||||
try:
|
||||
class_ = module_loading.import_string(step_config)
|
||||
except ImportError:
|
||||
LOG.error('Step class "%s" is not found (workflow "%s")',
|
||||
step_config, my_name)
|
||||
return
|
||||
self.register(class_)
|
||||
|
||||
def _gather_steps(self):
|
||||
ordered_step_classes = self._order_steps()
|
||||
for default_step in self.default_steps:
|
||||
@ -775,7 +802,7 @@ class Workflow(html.HTMLElement):
|
||||
if step_class in cls._cls_registry:
|
||||
return False
|
||||
else:
|
||||
cls._cls_registry.add(step_class)
|
||||
cls._cls_registry.append(step_class)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
|
@ -113,7 +113,8 @@ def update_dashboards(modules, horizon_config, installed_apps):
|
||||
xstatic_modules = []
|
||||
panel_customization = []
|
||||
header_sections = []
|
||||
extra_tabs = {}
|
||||
extra_tabs = collections.defaultdict(tuple)
|
||||
extra_steps = collections.defaultdict(tuple)
|
||||
update_horizon_config = {}
|
||||
for key, config in import_dashboard_config(modules):
|
||||
if config.get('DISABLED', False):
|
||||
@ -157,9 +158,12 @@ def update_dashboards(modules, horizon_config, installed_apps):
|
||||
elif config.get('PANEL') or config.get('PANEL_GROUP'):
|
||||
config.pop("__builtins__", None)
|
||||
panel_customization.append(config)
|
||||
_extra_tabs = config.get('EXTRA_TABS', {}).items()
|
||||
for tab_key, tab_defs in _extra_tabs:
|
||||
extra_tabs[tab_key] = extra_tabs.get(tab_key, tuple()) + tab_defs
|
||||
_extra_tabs = config.get('EXTRA_TABS', {})
|
||||
for tab_key, tab_defs in _extra_tabs.items():
|
||||
extra_tabs[tab_key] += tuple(tab_defs)
|
||||
_extra_steps = config.get('EXTRA_STEPS', {})
|
||||
for step_key, step_defs in _extra_steps.items():
|
||||
extra_steps[step_key] += tuple(step_defs)
|
||||
# Preserve the dashboard order specified in settings
|
||||
dashboards = ([d for d in config_dashboards
|
||||
if d not in disabled_dashboards] +
|
||||
@ -177,6 +181,7 @@ def update_dashboards(modules, horizon_config, installed_apps):
|
||||
horizon_config.setdefault('scss_files', []).extend(scss_files)
|
||||
horizon_config.setdefault('xstatic_modules', []).extend(xstatic_modules)
|
||||
horizon_config['extra_tabs'] = extra_tabs
|
||||
horizon_config['extra_steps'] = extra_steps
|
||||
|
||||
# apps contains reference to applications declared in the enabled folder
|
||||
# basically a list of applications that are internal and external plugins
|
||||
|
@ -0,0 +1,21 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Quota information panel and forms are now tabbified per back-end service.
|
||||
|
||||
- Admin -> Defaults -> Default Quotas table
|
||||
- Admin -> Defaults -> Update Defaults form
|
||||
- Identity -> Projects -> Modify Quotas form
|
||||
|
||||
- |
|
||||
[:blueprint:`horizon-plugin-tab-for-info-and-quotas`]
|
||||
(for horizon plugin developers) Django workflow step is now pluggable and
|
||||
horizon plugins can add extra step(s) to an existing workflow provided by
|
||||
horizon or other horizon plugins. Extra steps can be added via the horizon
|
||||
plugin “enabled” file. For more detail, see ``EXTRA_TABS`` description in
|
||||
`Pluggable Panels and Groups <https://docs.openstack.org/horizon/latest/configuration/pluggable_panels.html#extra-steps>`__
|
||||
of the horizon documentation.
|
||||
upgrade:
|
||||
- The "Quotas" tab in the "Create Project" form was split out into
|
||||
a new separate form "Modify Quotas". Quotas for a new project need to
|
||||
be configured from "Modify Quotas" action after creating a new project.
|
Loading…
Reference in New Issue
Block a user