New pgwui_menu.order setting which controls menu ordering
authorKarl O. Pinc <kop@karlpinc.com>
Fri, 20 Nov 2020 23:40:30 +0000 (17:40 -0600)
committerKarl O. Pinc <kop@karlpinc.com>
Fri, 20 Nov 2020 23:40:30 +0000 (17:40 -0600)
README.rst
setup.py
src/pgwui_menu/check_settings.py [new file with mode: 0644]
src/pgwui_menu/exceptions.py [new file with mode: 0644]
src/pgwui_menu/templates/menu.mak
src/pgwui_menu/views/menu.py
tests/test_check_settings.py [new file with mode: 0644]
tests/test_menu.py

index bda52b5b03615f7e9a922da49c2002a88a62743d..c4d2bfc2745d92645a99e07adad6186d36715b07 100644 (file)
@@ -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.
index e6c218cdfe05a07c8a0b09acdb444ab3af036795..b97b9d68fb6b607426e24d2535640b7452d98280 100644 (file)
--- 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 (file)
index 0000000..7128ffd
--- /dev/null
@@ -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
+# <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
diff --git a/src/pgwui_menu/exceptions.py b/src/pgwui_menu/exceptions.py
new file mode 100644 (file)
index 0000000..26ca00d
--- /dev/null
@@ -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
+# <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')
index c71ba474fbb8cf57be79ce9d2d92e3a623fd3dad..93372714f33206829132ed11c8dced4fac0017ab 100644 (file)
@@ -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
 
 
     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>
@@ -65,8 +60,6 @@
 
 <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>
index 1e0052c870a618fc250c4ff17f8e9abaa96fdad3..1e972a2e39822c0671b2a8d8abdbf118e8fb4a9b 100644 (file)
@@ -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 (file)
index 0000000..6160807
--- /dev/null
@@ -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
+# <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()
index ed1c1774e4eb1f53685839e78b96ec83871aaad0..4eb2f9405b2e0510664e08675259ffe8d2075001 100644 (file)
@@ -19,6 +19,7 @@
 
 # Karl O. Pinc <kop@karlpinc.com>
 
+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(