menu_label = upload -- Upload File Into Database
+The pgwui_menu component takes a list of pgwui component names
+and matches the menu order to the list order.
+
+ pgwui.pgwui_menu =
+ order = pgwui_upload
+ pgwui_logout
+
See the PGWUI_Common documentation if you wish to write your
own `Pyramid`_ application or wish more control and want to
configure using Python code.
},
# Setup an entry point to support PGWUI autoconfigure discovery.
- entry_points={'pgwui.components': '.pgwui_menu = pgwui_menu'}
+ entry_points={
+ 'pgwui.components': '.pgwui_menu = pgwui_menu',
+ 'pgwui.check_settings':
+ '.pgwui_menu = pgwui_menu.check_settings:check_settings'
+ }
)
--- /dev/null
+# Copyright (C) 2020 The Meme Factory, Inc. http://www.karlpinc.com/
+
+# This file is part of PGWUI_Menu.
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public
+# License along with this program. If not, see
+# <http://www.gnu.org/licenses/>.
+#
+
+# Karl O. Pinc <kop@karlpinc.com>
+
+from pgwui_common import checkset
+from pgwui_common import plugin
+from . import exceptions as menu_ex
+
+
+PGWUI_COMPONENT = 'pgwui_menu'
+MENU_SETTINGS = ['menu_label',
+ 'order',
+ ]
+REQUIRED_SETTINGS = []
+BOOLEAN_SETTINGS = []
+
+
+def validate_order(errors, components, settings):
+ '''Make sure the values are those allowed
+ '''
+ values = settings.get('order')
+ if values is None:
+ return
+ if isinstance(values, list):
+ for component in values:
+ if component not in components:
+ errors.append(menu_ex.BadOrderItemError(components, component))
+ else:
+ errors.append(menu_ex.BadOrderValuesError(values))
+
+
+def check_settings(component_config):
+ '''Check that all pgwui_upload specific settings are good.
+ This includes:
+ checking for unknown settings
+ checking for missing required settings
+ checking the boolean settings
+ checking that the values of other settings are valid
+ '''
+ errors = []
+ errors.extend(checkset.unknown_settings(
+ PGWUI_COMPONENT, MENU_SETTINGS, component_config))
+ errors.extend(checkset.require_settings(
+ PGWUI_COMPONENT, REQUIRED_SETTINGS, component_config))
+ errors.extend(checkset.boolean_settings(
+ PGWUI_COMPONENT, BOOLEAN_SETTINGS, component_config))
+
+ components = plugin.find_pgwui_components()
+ validate_order(errors, components, component_config)
+
+ return errors
--- /dev/null
+# Copyright (C) 2020 The Meme Factory, Inc. http://www.karlpinc.com/
+
+# This file is part of PGWUI_Menu.
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public
+# License along with this program. If not, see
+# <http://www.gnu.org/licenses/>.
+#
+
+# Karl O. Pinc <kop@karlpinc.com>
+
+from pgwui_common import exceptions as common_ex
+
+
+# PGWUI setting related exceptions
+
+class MenuError(common_ex.Error):
+ pass
+
+
+class BadOrderItemError(MenuError):
+ def __init__(self, components, value):
+ super().__init__(
+ 'The "pgwui.pgwui_menu.order" PGWUI setting must be '
+ f'a PGWUI component name, one of {components}; '
+ f'({value}) was supplied')
+
+
+class BadOrderValuesError(MenuError):
+ def __init__(self, value):
+ super().__init__(
+ 'The "pgwui.pgwui_menu.order" PGWUI setting must be '
+ f'a list of PGWUI component names; ({value}) was supplied')
menu_items A list of (name, url, label) tuples:
name The name of the pgwui_component
- url The url of the menu item, or None if no route exists
+ url The url of the menu item
conf The component's menu configuration, a dict with the keys:
menu_label The label of the menu item
base_mak = asset_abspath('pgwui_common:templates/base.mak')
%>
-<%
- # Sort menu items by pgwui component name, for lack of anything better
- # at present.
- menu_items.sort(key=lambda tup: tup[0])
-%>
<%inherit file="${base_mak}" />
<%block name="title">${pgwui_menu['menu_label']}</%block>
<ul>
% for (name, url, conf) in menu_items:
- % if name != 'pgwui_menu' and url:
- <li><a href="${url}">${conf['menu_label']}</a></li>
- % endif
+ <li><a href="${url}">${conf['menu_label']}</a></li>
% endfor
</ul>
from pgwui_common import plugin
+def build_menu(request, pgwui, menu_items, component):
+ '''Add a menu to menu_items, if there's a route'''
+ conf = pgwui.get(component)
+ if conf:
+ try:
+ route = request.route_url(component)
+ except KeyError:
+ pass
+ else:
+ menu_items.append((component, route, conf))
+
+
def build_menu_items(request, components):
- settings = request.registry.settings
+ # Don't put the menu on the menu
+ components.remove('pgwui_menu')
+ pgwui = request.registry.settings['pgwui']
menu_items = []
- for component in components:
- conf = settings['pgwui'].get(component)
- if conf:
- try:
- route = request.route_url(component)
- except KeyError:
- route = None
- menu_items.append((component, route, conf))
+ if 'order' in pgwui['pgwui_menu']:
+ for component in pgwui['pgwui_menu']['order']:
+ build_menu(request, pgwui, menu_items, component)
+ components.remove(component)
+ # Sort un-ordered menu items by pgwui component name, for lack of
+ # anything better at present.
+ components.sort()
+ for component in components:
+ build_menu(request, pgwui, menu_items, component)
return menu_items
--- /dev/null
+# Copyright (C) 2020 The Meme Factory, Inc. http://www.karlpinc.com/
+
+# This file is part of PGWUI_Menu.
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public
+# License along with this program. If not, see
+# <http://www.gnu.org/licenses/>.
+#
+
+# Karl O. Pinc <kop@karlpinc.com>
+
+import pgwui_menu.check_settings as check_settings
+
+from pgwui_common import checkset
+from pgwui_testing import testing
+from pgwui_menu import exceptions as menu_ex
+
+# Activiate our pytest plugin
+pytest_plugins = ("pgwui",)
+
+
+# Module packaging test
+
+def test_check_setting_is_pgwui_check_settings(
+ pgwui_check_settings_entry_point):
+ '''Ensure that pgwui_menu has a pgwui.check_settings entry point
+ '''
+ assert (pgwui_check_settings_entry_point('pgwui_menu.check_settings')
+ is True)
+
+
+# Mocks
+
+mock_unknown_settings = testing.make_mock_fixture(
+ checkset, 'unknown_settings')
+
+mock_require_settings = testing.make_mock_fixture(
+ checkset, 'require_settings')
+
+mock_boolean_settings = testing.make_mock_fixture(
+ checkset, 'boolean_settings')
+
+
+# validate_order
+
+def test_validate_order_nosetting():
+ '''No error is delivered when there's no setting'''
+ errors = []
+ check_settings.validate_order(errors, None, {})
+
+ assert errors == []
+
+
+def test_validate_order_ok():
+ '''No errors when all components in the ordering are ok
+ '''
+ errors = []
+ check_settings.validate_order(
+ errors, ['comp1', 'comp2'], {'order': ['comp1', 'comp2']})
+
+ assert errors == []
+
+
+def test_validate_order_singleton():
+ '''A non-list as an ordering gets the right error
+ '''
+ errors = []
+ check_settings.validate_order(
+ errors, None, {'order': 'string'})
+
+ assert errors
+ assert isinstance(errors[0], menu_ex.BadOrderValuesError)
+
+
+def test_validate_order_bad_component():
+ '''Deliver error when a bad component name is supplied in the ordering
+ '''
+ errors = []
+ check_settings.validate_order(
+ errors, ['comp1', 'comp2'], {'order': ['bad_component']})
+
+ assert errors
+ assert isinstance(errors[0], menu_ex.BadOrderItemError)
+
+
+order_err = 'order error'
+mock_validate_order = testing.make_mock_fixture(
+ check_settings, 'validate_order',
+ wraps=lambda errors, *args: errors.append(order_err))
+
+
+# check_settings()
+
+def test_check_settings(mock_unknown_settings,
+ mock_require_settings,
+ mock_boolean_settings,
+ mock_validate_order):
+ '''The setting checking functions are called once, the check_settings()
+ call returns all the errors from each mock.
+ '''
+
+ unknown_retval = ['unk err']
+ require_retval = ['req err']
+ boolean_retval = ['bool err']
+
+ mock_unknown_settings.return_value = unknown_retval
+ mock_require_settings.return_value = require_retval
+ mock_boolean_settings.return_value = boolean_retval
+
+ result = check_settings.check_settings({})
+
+ mock_unknown_settings.assert_called_once
+ mock_require_settings.assert_called_once
+ mock_boolean_settings.assert_called_once
+ mock_validate_order.assert_called_once
+
+ assert result.sort() == ([order_err]
+ + unknown_retval
+ + require_retval
+ + boolean_retval).sort()
# Karl O. Pinc <kop@karlpinc.com>
+import copy
import pyramid.testing
from pgwui_common import plugin, includeme
# Unit tests
-# build_menu_items()
+# build_menu()
+
+def test_build_menu_no_component():
+ '''When the plugin has no setting the menu is not modified
+ '''
+ menu_items = []
+ menu.build_menu(None, {}, menu_items, 'pgwui_dummy')
-def test_build_menu_items_component_with_route(mock_route_url):
+ assert menu_items == []
+
+
+def test_build_menu_component_with_route(mock_route_url):
'''When the plugin has a setting and a route the route is
- returned with the label'''
+ returned with the label
+ '''
test_route = '/test/route'
test_plugin = 'pgwui_plugin'
plugin_settings = {'key1': 'val1', 'key2': 'val2'}
- test_settings = {'pgwui': {test_plugin: plugin_settings}}
+ test_settings = {test_plugin: plugin_settings}
request = pyramid.testing.DummyRequest()
mocked_route_url = mock_route_url(request)
mocked_route_url.return_value = test_route
request.registry.settings = test_settings
- result = menu.build_menu_items(request, [test_plugin])
+ menu_items = []
+ menu.build_menu(request, test_settings, menu_items, test_plugin)
- assert result == [(test_plugin, test_route, plugin_settings)]
+ assert menu_items == [(test_plugin, test_route, plugin_settings)]
-def test_build_menu_items_component_no_route(mock_route_url):
- '''When the plugin has a setting and no route, None is returned
- as the the route with the label'''
- test_route = None
+def test_build_menu_component_no_route(mock_route_url):
+ '''When the plugin has a setting and no route, the menu is not modified
+ '''
test_plugin = 'pgwui_plugin'
plugin_settings = {'key1': 'val1', 'key2': 'val2'}
- test_settings = {'pgwui': {test_plugin: plugin_settings}}
+ test_settings = {test_plugin: plugin_settings}
request = pyramid.testing.DummyRequest()
mocked_route_url = mock_route_url(request)
mocked_route_url.side_effect = KeyError
request.registry.settings = test_settings
- result = menu.build_menu_items(request, [test_plugin])
+ menu_items = []
+ menu.build_menu(request, test_settings, menu_items, test_plugin)
- assert result == [(test_plugin, test_route, plugin_settings)]
+ assert menu_items == []
-def test_build_menu_items_component_no_plugin_conf():
- '''When the plugin has no configuration it is not added to the
- returned menu items'''
- test_plugin = 'pgwui_plugin'
- test_settings = {'pgwui': {}}
+mock_build_menu = testing.make_mock_fixture(
+ menu, 'build_menu')
+
+
+# build_menu_items()
+
+def test_build_menu_items_component_no_plugin_conf(mock_build_menu):
+ '''Components given in the pgwui_menu.order setting are added
+ in order, other components are added alphabetically, the pgwui_menu
+ component is not added
+ '''
+ components = ['plugin5', 'plugin4', 'pgwui_menu', 'plugin2', 'plugin1']
+ orig_components = copy.deepcopy(components)
+ test_settings = {'pgwui':
+ {'pgwui_menu':
+ {'order': ['plugin5', 'plugin2']}}}
+
+ request = pyramid.testing.DummyRequest()
+ request.registry.settings = test_settings
+
+ menu.build_menu_items(request, components)
+
+ call_args = mock_build_menu.call_args_list
+ # Check the order of the ordered components
+ assert call_args[0][0][3] == 'plugin5'
+ assert call_args[1][0][3] == 'plugin2'
+ # The pgwui_menu is ignored
+ assert len(call_args) == len(orig_components) - 1
+ # Check that order of the remaining components is alphabetical
+ assert call_args[2][0][3] == 'plugin1'
+ assert call_args[3][0][3] == 'plugin4'
+
+
+def test_build_menu_items_no_order(mock_build_menu):
+ '''When there is no order setting components are added alphabetically
+ '''
+ components = ['plugin5', 'plugin4', 'pgwui_menu', 'plugin2', 'plugin1']
+ orig_components = copy.deepcopy(components)
+ test_settings = {'pgwui':
+ {'pgwui_menu': {}}}
request = pyramid.testing.DummyRequest()
request.registry.settings = test_settings
- result = menu.build_menu_items(request, [test_plugin])
+ menu.build_menu_items(request, components)
- assert result == []
+ call_args = mock_build_menu.call_args_list
+ # The pgwui_menu is ignored
+ assert len(call_args) == len(orig_components) - 1
+ # Check the order of the ordered components
+ assert call_args[0][0][3] == 'plugin1'
+ assert call_args[1][0][3] == 'plugin2'
+ assert call_args[2][0][3] == 'plugin4'
+ assert call_args[3][0][3] == 'plugin5'
mock_build_menu_items = testing.make_mock_fixture(