From 65f3b2003ae425bb3f475267f50726c84d574e35 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Fri, 20 Nov 2020 17:40:30 -0600 Subject: [PATCH] New pgwui_menu.order setting which controls menu ordering --- README.rst | 7 ++ setup.py | 6 +- src/pgwui_menu/check_settings.py | 68 ++++++++++++++++ src/pgwui_menu/exceptions.py | 43 ++++++++++ src/pgwui_menu/templates/menu.mak | 11 +-- src/pgwui_menu/views/menu.py | 33 +++++--- tests/test_check_settings.py | 130 ++++++++++++++++++++++++++++++ tests/test_menu.py | 94 ++++++++++++++++----- 8 files changed, 353 insertions(+), 39 deletions(-) create mode 100644 src/pgwui_menu/check_settings.py create mode 100644 src/pgwui_menu/exceptions.py create mode 100644 tests/test_check_settings.py diff --git a/README.rst b/README.rst index bda52b5..c4d2bfc 100644 --- a/README.rst +++ b/README.rst @@ -77,6 +77,13 @@ An example configuration directive for PGWUI_Upload:: 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. diff --git a/setup.py b/setup.py index e6c218c..b97b9d6 100644 --- a/setup.py +++ b/setup.py @@ -167,5 +167,9 @@ setup( }, # 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' + } ) diff --git a/src/pgwui_menu/check_settings.py b/src/pgwui_menu/check_settings.py new file mode 100644 index 0000000..7128ffd --- /dev/null +++ b/src/pgwui_menu/check_settings.py @@ -0,0 +1,68 @@ +# 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 +# . +# + +# Karl O. Pinc + +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 diff --git a/src/pgwui_menu/exceptions.py b/src/pgwui_menu/exceptions.py new file mode 100644 index 0000000..26ca00d --- /dev/null +++ b/src/pgwui_menu/exceptions.py @@ -0,0 +1,43 @@ +# 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 +# . +# + +# Karl O. Pinc + +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') diff --git a/src/pgwui_menu/templates/menu.mak b/src/pgwui_menu/templates/menu.mak index c71ba47..9337271 100644 --- a/src/pgwui_menu/templates/menu.mak +++ b/src/pgwui_menu/templates/menu.mak @@ -26,7 +26,7 @@ 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 @@ -39,11 +39,6 @@ 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']} @@ -65,8 +60,6 @@ diff --git a/src/pgwui_menu/views/menu.py b/src/pgwui_menu/views/menu.py index 1e0052c..1e972a2 100644 --- a/src/pgwui_menu/views/menu.py +++ b/src/pgwui_menu/views/menu.py @@ -27,19 +27,34 @@ from pgwui_common.pgwui_common import base_view 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 diff --git a/tests/test_check_settings.py b/tests/test_check_settings.py new file mode 100644 index 0000000..6160807 --- /dev/null +++ b/tests/test_check_settings.py @@ -0,0 +1,130 @@ +# 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 +# . +# + +# Karl O. Pinc + +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() diff --git a/tests/test_menu.py b/tests/test_menu.py index ed1c177..4eb2f94 100644 --- a/tests/test_menu.py +++ b/tests/test_menu.py @@ -19,6 +19,7 @@ # Karl O. Pinc +import copy import pyramid.testing from pgwui_common import plugin, includeme @@ -36,56 +37,109 @@ mock_route_url = testing.instance_method_mock_fixture('route_url') # 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( -- 2.34.1