Add home_page and menu_page concepts
authorKarl O. Pinc <kop@karlpinc.com>
Tue, 1 Dec 2020 02:55:01 +0000 (20:55 -0600)
committerKarl O. Pinc <kop@karlpinc.com>
Tue, 1 Dec 2020 03:16:19 +0000 (21:16 -0600)
README.rst
examples/etc/pgwui.ini
examples/misc/development.ini
src/pgwui_server/checkset.py
src/pgwui_server/exceptions.py
src/pgwui_server/pgwui_server.py
tests/test_checkset.py
tests/test_pgwui_server.py

index 4076584e13d42a6c442c53e4beccbd1e83028f71..35bca56d391ca5315079df7e7f41eb8dc11581c0 100644 (file)
@@ -221,6 +221,66 @@ Manual specification, with or without autoconfiguration, can be
 convenient when writing your own components.  (It also eliminates the
 trivial overhead involved in autoconfiguration.)
 
+Configuring Navbar Links
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+Navbar link configuration is optional.  PGWUI comes with sensible
+defaults.
+
+ pgwui.home_page::  How to link to the site's home page.
+   type:  URL (default), asset, file
+       
+   URL
+     source:
+
+       The default is ``/``, when there is no home_page setting.
+       Which produces an URL with no "path".
+     
+       * A URI path beginning with ``/``.  E.g.: '/home'
+
+       * An URL without a protocol, so an URL beginning with ``//`` and
+         followed by a domain.  E.g.: //www.example.com  The URL
+         delivered to the browser contains the protocol used in the request.
+
+       * An URL with a protocol.  E.g.: https://www.example.com
+
+   file:
+     source:
+       A fully-qualified file system path, so a path beginning with
+       a ``/``.  E.g.  /var/www/html/index.html
+       Served with a content encoding of ``text/html``.
+
+   asset:
+     source:
+       * A `Pyramid`_ `asset specification`_.  It must reference a
+         `static asset`_, a file included in a `Pyramid`_ application.
+         Typically file containing a page of HTML.
+
+         This is only useful to users who write their own `Pyramid`_
+         applications that are either PGWUI modules or incorporate
+         PGWUI.
+       
+   route:
+     source:
+       * A `Pyramid`_ `route name`_.  Used to reach a page generated
+         by a `Pyramid`_ application which uses `URL dispatch`_.
+
+         This is only useful to users who write their own `Pyramid`_
+         applications that incorporate PGWUI.
+
+ pgwui.menu_page::  How to link to a menu of PGWUI components.
+ All of the "type"s of ``pgwui.home_page`` are available.
+
+
+Configuration Settings Common to All PGWUI Components
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+PGWUI modules all have the following configuration settings:
+
+ menu_label
+   The label for PGWUI_Menu to display, when different from the default
+
+
 Configuring Routing
 ^^^^^^^^^^^^^^^^^^^
 
@@ -272,21 +332,26 @@ HTML templates are used to generate the page's HTML.  Templates
 control what is displayed.
 
 Here is a list of the current asset specifications with brief
-descriptions::
+descriptions:
 
-  pgwui_common:static/pgwui.css The CSS file for PGWUI.
+  pgwui_common:static/pgwui.css
+    The CSS file for PGWUI.
 
-  pgwui_common:templates/base.mak  Common "background" items on all pages.
+  pgwui_common:templates/base.mak
+    Common "background" items on all pages.
 
-  pgwui_common:templates/auth_base.mak  Common "background" items on all
-  pages requesting database connection and login information.
+  pgwui_common:templates/auth_base.mak
+    Common "background" items on all
+    pages requesting database connection and login information.
 
-  pgwui_logout:templates/logout.mak  The logout page.
+  pgwui_logout:templates/logout.mak
+    The logout page.
 
-  pgwui_upload:templates/upload.mak  The upload page.
+  pgwui_upload:templates/upload.mak
+    The upload page.
 
 Assets can be overridden in the configuration file with
-``pgwui.override_asset``:
+``pgwui.override_asset``::
 
     pgwui.override_asset =
         # Syntax is: asset_to_override = override_with
@@ -530,6 +595,11 @@ provided by `The Dian Fossey Gorilla Fund
 .. _WSGI: https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface
 .. _pip: https://pip.pypa.io/en/stable/
 
+.. _asset specification: https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/assets.html#understanding-asset-specifications
+.. _static asset: https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/assets.html#serving-static-assets
+.. _route name: https://docs.pylonsproject.org/projects/pyramid/en/1.10-branch/narr/urldispatch.html#route-configuration
+.. _URL dispatch: https://docs.pylonsproject.org/projects/pyramid/en/1.10-branch/narr/urldispatch.html
+
 
 .. rubric:: Footnotes
 
index 969a4dc87e94058c5c228ff557b3986da4491cf5..1eaa1eb659c9fa11e0f5b985b6cbbc4037361133 100644 (file)
@@ -17,7 +17,7 @@ use = egg:PGWUI_Server
 # PGWUI configuration
 #
 
-# Postgres client configuration.
+# Postgres connection configuration.
 
 # Both pgwui.pg_host and pgwui.pg_port are optional;
 # they default to the Unix PG socket and the default Postgres port.
@@ -28,6 +28,84 @@ pgwui.pg_port = 5432
 # There are occasions when PGWUI uses a default database.  (optional)
 pgwui.default_db = template1
 
+
+# How to link to the site's home page.
+#
+# This is a multi-valued setting.  The keys depend on the "type"
+# used.
+#
+#   type:
+#     URL    An URL.  This is the default.
+#     file   A file containing HTML, read from the file system.
+#     asset  A Pyramid asset specification.  Results in an URL
+#            which references a file which is part of a PGWUI
+#            component, or some other "static" file included in a Pyramid
+#            application.
+#     route  A Pyramid route name.  Results in an URL
+#            which references a page generated by a Pyramid application.
+#            "pgwui.route_prefix" is not applied.
+#
+#
+#   When "type" is "URL, there are the following keys:
+#
+#   source:  (required)
+#
+#       The default is ``/``, when there is no home_page setting.
+#       Which produces an URL with no "path".
+#     
+#       * A URI path beginning with ``/``.  E.g.: '/home'
+#
+#       * An URL without a protocol, so an URL beginning with ``//`` and
+#       followed by a domain.  E.g.: //www.example.com  The URL
+#       delivered to the browser contains the protocol used in the request.
+#
+#       * An URL with a protocol.  E.g.: https://www.example.com
+#
+#
+#  When "type" is "file", there are the following keys
+#
+#  source: (required)
+#       A fully-qualified file system path, so a path beginning with
+#       a ``/``.  E.g.  /var/www/html/index.html
+#       Served with a content encoding of ``text/html``.
+#
+#  url_path: (required)
+#      The "path" component of the URL used to retrieve the file.
+#      Must begin with a ``/``.  "pgwui.route_prefix" is not applied.
+#
+#  When type is "asset", there are the following keys:
+#
+#  source: (required)
+#
+#      * A `Pyramid`_ `asset specification`_.  It must reference a
+#      `static asset`_, a file included in a `Pyramid`_ application.
+#      A file containing a page of HTML.
+#
+#
+#  When type is "route", there are the following keys:
+#
+#  source: (required)
+#
+#      * A `Pyramid`_ `route name`_.  Used to reach a page generated
+#      by a `Pyramid`_ application which uses `URL dispatch`_.
+#
+#
+# pgwui.home_page =
+#   type = URL
+#   source = /
+
+# How to link to a menu of PGWUI components.  An alternative to using
+# the PGWUI_Menu component.
+# Configured as pgwui.home_page is configured, above.
+# The default is to have no menu.  The following uses the URL
+# without a path (e.g., http://www.example.com/) as the menu page.
+# 
+#       pgwui.menu_page =
+#         type = URL
+#         source = /
+#
+# pgwui.menu_page overrides what is provided by the PGWUI_Menu component.
+
 # Whether to auto-discover the pgwui component modules. (optional)
 # When False pgwui component names must be listed in pyramid.includes=...
 # pgwui.autoconfigure = True
@@ -43,7 +121,7 @@ pgwui.dry_run = False
 
 # Routing
 
-# Routes are what call up specific pages.  They are the
+# Routes are what call up specific pages.  They are usually the
 # part of the URL which comes after the http://example.com.
 #
 # All routes should probably begin with a "/" character but
@@ -71,8 +149,8 @@ pgwui.dry_run = False
 #
 # The default for some PGWUI components are:
 # pgwui.routes =
-#   pgwui_logout = /logmeout
-#   pgwui_upload = /put-in
+#   pgwui_logout = /logout
+#   pgwui_upload = /upload
 
 # Settings validation
 
@@ -81,6 +159,7 @@ pgwui.dry_run = False
 # vulnerabilties.  Validation is on by default.
 # pgwui.validate_hmac = True
 
+
 # PGWUI Component Settings
 
 # Menu presentation
index 0d069bd39fdcc6c7054874f425f3123ef526150f..9ea841f34e3e5a0caf8b91da93a8edd71ad74bf8 100644 (file)
@@ -17,7 +17,7 @@ use = egg:PGWUI_Server
 # PGWUI configuration
 #
 
-# Postgres client configuration.
+# Postgres connection configuration.
 
 # Both pgwui.pg_host and pgwui.pg_port are optional;
 # they default to the Unix PG socket and the default Postgres port.
@@ -29,12 +29,95 @@ pgwui.pg_port = 5432
 pgwui.default_db = template1
 
 
+# How to link to the site's home page.  Useful when the home page is
+# not the PGWUI menu.
+#
+# This is a multi-valued setting.  The keys depend on the "type"
+# used.
+#
+#   type:
+#     URL    An URL.  This is the default.
+#     file   A file containing HTML, read from the file system.
+#     asset  A Pyramid asset specification.  Results in an URL
+#            which references a file which is part of a PGWUI
+#            component, or some other "static" file included in a Pyramid
+#            application.
+#     route  A Pyramid route name.  Results in an URL
+#            which references a page generated by a Pyramid application.
+#            "pgwui.route_prefix" is not applied.
+#
+#
+#   When "type" is "URL, there are the following keys:
+#
+#   source:  (required)
+#
+#       The default is ``/``, when there is no home_page setting.
+#       Which produces an URL with no "path".
+#     
+#       * A URI path beginning with ``/``.  E.g.: '/home'
+#
+#       * An URL without a protocol, so an URL beginning with ``//`` and
+#       followed by a domain.  E.g.: //www.example.com  The URL
+#       delivered to the browser contains the protocol used in the request.
+#
+#       * An URL with a protocol.  E.g.: https://www.example.com
+#
+#
+#  When "type" is "file", there are the following keys
+#
+#  source: (required)
+#       A fully-qualified file system path, so a path beginning with
+#       a ``/``.  E.g.  /var/www/html/index.html
+#       Served with a content encoding of ``text/html``.
+#
+#  url_path: (required)
+#      The "path" component of the URL used to retrieve the file.
+#      Must begin with a ``/``.  "pgwui.route_prefix" is not applied.
+#
+#  When type is "asset", there are the following keys:
+#
+#  source: (required)
+#
+#      * A `Pyramid`_ `asset specification`_.  It must reference a
+#      `static asset`_, a file included in a `Pyramid`_ application.
+#      A file containing a page of HTML.
+#
+#
+#  When type is "route", there are the following keys:
+#
+#  source: (required)
+#
+#      * A `Pyramid`_ `route name`_.  Used to reach a page generated
+#      by a `Pyramid`_ application which uses `URL dispatch`_.
+#
+#
+# pgwui.home_page =
+#   type = URL
+#   source = /
+
+
+# How to link to a menu of PGWUI components.  An alternative to using
+# the PGWUI_Menu component.
+# Configured as pgwui.home_page is configured, above.
+# The default is to have no menu.  The following uses the URL
+# without a path (e.g., http://www.example.com/) as the menu page.
+# 
+#       pgwui.menu_page =
+#         type = URL
+#         source = /
+#
+# pgwui.menu_page overrides what is provided by the PGWUI_Menu component.
+
 # Whether to auto-discover the pgwui component modules. (optional)
 # When False pgwui component names must be listed in pyramid.includes=...
 # pgwui.autoconfigure = True
 
 # What PGWUI components and other pyramid modules to use.
-# (Required when pgwui.autoconfigure is False.)
+# (Required when pgwui.autoconfigure is False, or you want the
+# debug toolbar.)
+#pyramid.includes =
+#    pgwui_logout
+#    pgwui_upload
 pyramid.includes =
     pyramid_debugtoolbar
 
@@ -81,6 +164,7 @@ pgwui.dry_run = False
 # vulnerabilties.  Validation is on by default.
 pgwui.validate_hmac = False
 
+
 # PGWUI Component Settings
 
 # Menu presentation
@@ -150,7 +234,6 @@ session.key = pgwui_server
 # HMAC secret
 #session.secret = xxxxxxrandomstring40characterslongxxxxxx
 # Send cookie only over https
-# (True for production)
 # WARNING: To use HTTP, not HTTPS, session.secure must be False!
 # CAUTION: If you are forcing the browser to use HTTPS you want
 #          session.secure to be True.
index 62746ba5f499aaf9f80923f13fea0f3cc67a263d..c1fc14e876ce92470fe9962145f2666fddbf921e 100644 (file)
 
 # Karl O. Pinc <kop@karlpinc.com>
 
-'''Check the pgwui settings (in the internal "dict format")
+'''Validate PGWUI_Core and PGWUI_Common configuration
 '''
 
+import re
 from ast import literal_eval
 
 from . import constants
 from pgwui_common import exceptions as common_ex
+from pgwui_common import checkset
 import pgwui_server.exceptions as server_ex
 
 
+# Regular expressions for page "source" values, by type
+URL_RE = re.compile('^(?:(?:[^:/]+:)?//[^/])|(?:/(?:[^/]|$))')
+
+
 def key_to_ini(key):
     '''Convert the setting key to a key used in an ini file's declaration
     '''
-    return 'pgwui.{}'.format(key)
+    return 'pgwui:{}'.format(key)
 
 
-def require_setting(errors, setting, pgwui_settings):
+def require_setting(errors, setting, pgwui_settings, formatter):
     if setting not in pgwui_settings:
-        errors.append(common_ex.MissingSettingError(key_to_ini(setting)))
+        errors.append(common_ex.MissingSettingError(formatter(setting)))
+        return False
+    return True
 
 
 def boolean_setting(errors, setting, pgwui_settings):
@@ -65,7 +73,7 @@ def validate_setting_values(errors, settings):
     # default_db can be missing, then the user sees no default
 
     # dry_run
-    require_setting(errors, 'dry_run', pgwui_settings)
+    require_setting(errors, 'dry_run', pgwui_settings, key_to_ini)
     boolean_setting(errors, 'dry_run', pgwui_settings)
 
     # route_prefix can be missing, defaults to no route prefix which is fine.
@@ -96,3 +104,127 @@ def validate_hmac(errors, settings):
     if len(settings['session.secret']) != constants.HMAC_LEN:
         errors.append(server_ex.HMACLengthError())
         return
+
+
+def page_key_to_ini(page_key, subkey):
+    '''Convert the page setting subkey to a ini file declaration
+    '''
+    return key_to_ini(f'{page_key}:{subkey}')
+
+
+def require_page_settings(errors, required_settings, page_settings, page_key):
+    '''Check for required keys in the page setting
+    '''
+    def subkey_to_ini(subkey):
+        return page_key_to_ini(page_key, subkey)
+
+    have_settings = True
+    for subkey in required_settings:
+        have_settings &= require_setting(
+            errors, subkey, page_settings, subkey_to_ini)
+
+    return have_settings
+
+
+def validate_url_source(errors, page_key, source):
+    '''Validate the page setting "source" for URLs
+    '''
+    if URL_RE.match(source):
+        return
+    errors.append(common_ex.BadURLSourceError(
+        page_key_to_ini(page_key, 'source'), source))
+
+
+def validate_url_path(errors, page_key, page_settings):
+    '''Validate the page setting "url_path"
+    '''
+    url_path = page_settings['url_path']
+    if url_path[0:1] == '/':
+        return
+    errors.append(common_ex.BadFileURLPathError(
+        page_key_to_ini(page_key, 'url_path'), url_path))
+
+
+def validate_file_source(errors, page_key, source):
+    '''Validate the page setting "source" for files
+    '''
+    if source[0:1] == '/':
+        return
+    errors.append(common_ex.BadFileSourceError(
+        page_key_to_ini(page_key, 'file'), source))
+
+
+def validate_route_source(errors, page_key, source):
+    '''Validate the page setting "source" for routes
+
+    The routes are not yet established, so we don't confirm
+    existance at this point.
+    '''
+    if source != '':
+        return
+    errors.append(common_ex.BadRouteSourceError(
+        page_key_to_ini(page_key, 'route'), source))
+
+
+def validate_asset_source(errors, page_key, source):
+    '''Validate the page setting "source" for assets
+    '''
+    if source != '':
+        return
+    errors.append(common_ex.BadAssetSourceError(
+        page_key_to_ini(page_key, 'asset'), source))
+
+
+def validate_file_content(errors, page_key, page_settings, source):
+    '''Validate the content of a "file" page setting
+    '''
+    validate_file_source(errors, page_key, source)
+    if require_page_settings(
+            errors, ['url_path'], page_settings, page_key):
+        validate_url_path(errors, page_key, page_settings)
+    errors.extend(checkset.unknown_settings(
+        f'pgwui:{page_key}', ['type', 'source', 'url_path'], page_settings))
+
+
+def validate_type_content(errors, page_key, page_settings):
+    '''Validate the page setting's "type", and other page setting content
+    based on the type
+    '''
+    type = page_settings['type']
+    source = page_settings['source']
+    if type == 'URL':
+        validate_url_source(errors, page_key, source)
+        errors.extend(checkset.unknown_settings(
+            'pgwui_common', ['type', 'source'], page_settings))
+        return
+    if type == 'file':
+        validate_file_content(errors, page_key, page_settings, source)
+        return
+    if type == 'route':
+        validate_route_source(errors, page_key, source)
+        errors.extend(checkset.unknown_settings(
+            'pgwui_common', ['type', 'source'], page_settings))
+        return
+    if type == 'asset':
+        validate_asset_source(errors, page_key, source)
+        errors.extend(checkset.unknown_settings(
+            'pgwui_common', ['type', 'source'], page_settings))
+        return
+
+    errors.append(common_ex.BadPageTypeError(
+        page_key_to_ini(page_key, 'type'), type))
+
+
+def validate_page_setting(errors, settings, page_key):
+    '''Validate the multiple values of the page setting
+    '''
+    pgwui_settings = settings['pgwui']
+    if page_key not in pgwui_settings:
+        return
+
+    page_settings = pgwui_settings[page_key]
+    if not require_page_settings(
+            errors, ['type', 'source'], page_settings, page_key):
+        return
+
+    validate_type_content(errors, page_key, page_settings)
index 518ac0fe622117feefa13f474759e5dc5efd853b..a2c3ea7b93d29e34c44fc1a7061aab619b749d24 100644 (file)
@@ -36,6 +36,13 @@ class AutoconfigureConflict(ServerError):
             'Autoconfigure is True and there is a pyramid.include setting')
 
 
+class MenuPageInRoutes(ServerError):
+    def __init__(self):
+        super().__init__(
+            'The pgwui_menu in the pgwui.routes setting is ignored '
+            'and the pgwui.menu_page setting used instead')
+
+
 class BadSettingsAbort(ServerError):
     def __init__(self):
         super().__init__('Aborting due to bad setting(s)')
index 776e3fdd7fe52961cc58630bd123210aedc8fe1b..269102973589f5f687d2b93a4d968e64c380ccbb 100644 (file)
@@ -20,7 +20,8 @@
 
 # Karl O. Pinc <kop@karlpinc.com>
 
-'''Provide a way to configure PGWUI.
+'''Load the PGWUI components, parse the PGWUI configuration, and start the
+WSGI server.
 '''
 
 from pyramid.config import Configurator
@@ -34,7 +35,7 @@ from pgwui_common import plugin
 
 # Constants
 
-# All the settings recognized by PGWUI
+# All the single-valued settings recognized by PGWUI_Server/Core
 SETTINGS = set(
     ['pg_host',
      'pg_port',
@@ -46,6 +47,18 @@ SETTINGS = set(
      'autoconfigure',
      ])
 
+# All the multi-valued settings recognized by PGWUI_Server/Core
+MULTI_SETTINGS = set(
+    ['home_page',
+     'menu_page',
+     ])
+
+# Default settings
+DEFAULT_HOME_PAGE_TYPE = 'URL'
+DEFAULT_HOME_PAGE_SOURCE = '/'
+DEFAULT_SETTINGS = {  # As delivered by configparser to this parser
+    'pgwui.home_page': f'type = {DEFAULT_HOME_PAGE_TYPE}\n'
+                       f'source = {DEFAULT_HOME_PAGE_SOURCE}\n'}
 
 # Logging
 log = logging.getLogger(__name__)
@@ -53,25 +66,6 @@ log = logging.getLogger(__name__)
 
 # Functions
 
-def dot_to_component_settings(settings, key, component):
-    '''Put a component's settings into its own dict,
-    adding to what's already there
-    '''
-    comp_settings = settings['pgwui'].setdefault(component, dict())
-    comp_settings.update(settings[key])
-    del settings[key]
-
-
-def component_setting_into_dict(
-        errors, component_checkers, key, settings, component):
-    '''Put a component's settings in its own dict and validate them
-    '''
-    comp_settings = dot_to_component_settings(settings, key, component)
-    if component in component_checkers:
-        errors.extend(
-            component_checkers[component](comp_settings))
-
-
 def dot_to_dict(settings, key, new_key):
     settings['pgwui'][new_key] = settings[key]
     del settings[key]
@@ -107,6 +101,25 @@ def parse_assignments(lines):
     return result
 
 
+def dot_to_multiline_setting(settings, key, pgwui_key):
+    '''Put a multi-line setting into its own dict,
+    adding to what's already there
+    '''
+    multi_setting = settings['pgwui'].setdefault(pgwui_key, dict())
+    multi_setting.update(dict(parse_assignments(settings[key])))
+    del settings[key]
+
+
+def component_setting_into_dict(
+        errors, component_checkers, key, settings, component):
+    '''Put a component's settings in its own dict and validate them
+    '''
+    comp_settings = dot_to_multiline_setting(settings, key, component)
+    if component in component_checkers:
+        errors.extend(
+            component_checkers[component](comp_settings))
+
+
 def setting_into_dict(
         errors, components, component_checkers, key, settings):
     '''Separate a pgwui setting into a dict on '.' chars; validate
@@ -115,18 +128,19 @@ def setting_into_dict(
     if key[:6] == 'pgwui.':
         new_key = key[6:]
         if new_key in components:
-            settings[key] = dict(parse_assignments(settings[key]))
             component_setting_into_dict(
                 errors, component_checkers, key, settings, new_key)
         else:
             if new_key in SETTINGS:
                 dot_to_dict(settings, key, new_key)
+            elif new_key in MULTI_SETTINGS:
+                dot_to_multiline_setting(settings, key, new_key)
             else:
                 errors.append(common_ex.UnknownSettingKeyError(key))
 
 
 def dictify_settings(errors, settings, components):
-    '''Convert . in the pgwui settings to dict mappings, and validate
+    '''Convert "." in the pgwui settings to dict mappings, and validate
     the result.
     '''
     component_checkers = plugin.find_pgwui_check_settings()
@@ -136,6 +150,8 @@ def dictify_settings(errors, settings, components):
             errors, components, component_checkers, key, settings)
     checkset.validate_setting_values(errors, settings)
     checkset.validate_hmac(errors, settings)
+    checkset.validate_page_setting(errors, settings, 'home_page')
+    checkset.validate_page_setting(errors, settings, 'menu_page')
 
 
 def exit_reporting_errors(errors):
@@ -153,9 +169,17 @@ def exit_reporting_errors(errors):
     sys.exit(1)
 
 
+def add_default_settings(settings):
+    '''Add the default settings to the config if not there
+    '''
+    for setting, val in DEFAULT_SETTINGS.items():
+        settings.setdefault(setting, val)
+
+
 def exit_on_invalid_settings(settings, components):
     '''Exit when settings don't validate
     '''
+    add_default_settings(settings)
     errors = []
     dictify_settings(errors, settings, components)
     if errors:
@@ -167,9 +191,13 @@ def add_routes(config, settings):
     '''
     pgwui_settings = settings['pgwui']
     if 'routes' in pgwui_settings:
+        menu_page = 'menu_page' in pgwui_settings
         routes = parse_assignments(pgwui_settings['routes'])
         for name, route in routes:
-            config.add_route(name, route)
+            if menu_page and name == 'pgwui_menu':
+                log.info(server_ex.MenuPageInRoutes())
+            else:
+                config.add_route(name, route)
 
 
 def autoconfigurable_components(settings, components):
index 58be20b45ea229de5a8442a13e03e077b5800572..86aaf13f2d9b91024ff71889171881a1b9cabd14 100644 (file)
 import pytest
 
 import pgwui_common.exceptions as common_ex
+import pgwui_common
 from pgwui_server import checkset
 import pgwui_server.constants as constants
 from pgwui_server import exceptions as server_ex
 from pgwui_testing import testing
 
+mock_unknown_settings = testing.make_mock_fixture(
+    pgwui_common.checkset, 'unknown_settings')
+
 
 # key_to_ini()
 
@@ -38,7 +42,7 @@ def test_key_to_ini():
     key = 'pgwui_example'
     result = checkset.key_to_ini(key)
 
-    assert result == 'pgwui.' + key
+    assert result == 'pgwui:' + key
 
 
 mock_key_to_ini = testing.make_mock_fixture(
@@ -51,7 +55,7 @@ mock_key_to_ini = testing.make_mock_fixture(
 def test_require_setting_missing():
     '''Deliver exception when a required setting is missing'''
     errors = []
-    checkset.require_setting(errors, 'key', {})
+    checkset.require_setting(errors, 'key', {}, lambda x: x)
 
     assert errors
     assert isinstance(errors[0], common_ex.MissingSettingError)
@@ -61,7 +65,7 @@ def test_require_setting_missing():
 def test_require_setting_present():
     '''Does nothing when a required setting is present'''
     errors = []
-    checkset.require_setting(errors, 'key', {'key': 'value'})
+    checkset.require_setting(errors, 'key', {'key': 'value'}, lambda x: x)
 
     assert errors == []
 
@@ -216,3 +220,371 @@ def test_validate_hmac_length(mock_do_validate_hmac):
 
 mock_validate_hmac = testing.make_mock_fixture(
     checkset, 'validate_hmac')
+
+
+# page_key_to_ini()
+
+@pytest.mark.unittest
+def test_page_key_to_ini(mock_key_to_ini):
+    '''key_to_ini() is called, expected result returned
+    '''
+    mock_key_to_ini.return_value = 'foo'
+    result = checkset.page_key_to_ini(None, None)
+    assert result == 'foo'
+
+
+mock_page_key_to_ini = testing.make_mock_fixture(
+    checkset, 'page_key_to_ini')
+
+
+# require_page_settings()
+
+@pytest.mark.parametrize(
+    ('required_settings', 'rs_results', 'expected'), [
+        # Settings exist, return True
+        (['s1', 's2'], [True, True], True),
+        # One setting does not exist, return False
+        (['s1', 's2'], [True, False], False)])
+@pytest.mark.unittest
+def test_require_page_settings_result(
+        mock_page_key_to_ini, mock_require_setting,
+        required_settings, rs_results, expected):
+    '''Returns the expected result
+    '''
+    mock_require_setting.side_effect = rs_results
+    result = checkset.require_page_settings(
+        None, required_settings, None, None)
+    assert result == expected
+
+
+@pytest.mark.unittest
+def test_require_page_settings_subfunc(
+        mock_page_key_to_ini, mock_require_setting):
+    '''Calls page_key_to_ini() when function is passed to require_setting()
+    '''
+    def mock_rs(x, subkey, z, subkey_to_ini):
+        subkey_to_ini(subkey)
+        return True
+
+    required_settings = ['s1', 's2']
+    mock_require_setting.side_effect = mock_rs
+    checkset.require_page_settings(None, required_settings, None, None)
+
+    assert mock_page_key_to_ini.call_count == len(required_settings)
+
+
+mock_require_page_settings = testing.make_mock_fixture(
+    checkset, 'require_page_settings')
+
+
+# validate_url_source()
+
+@pytest.mark.parametrize(
+    ('source', 'expected_error'), [
+        ('/', None),
+        ('/foo', None),
+        ('//www.example.com', None),
+        ('//www.example.com/', None),
+        ('//www.example.com/foo', None),
+        ('http://www.example.com', None),
+        ('https://www.example.com', None),
+        ('anything://www.example.com', None),
+        ('http://www.example.com/', None),
+        ('http://www.example.com/foo', None),
+        # No domain
+        ('//', common_ex.BadURLSourceError),
+        # Nothing
+        ('', common_ex.BadURLSourceError),
+        # Missing / after scheme
+        ('http:/www.example.com', common_ex.BadURLSourceError),
+        # Extra / after scheme
+        ('http:///www.example.com', common_ex.BadURLSourceError)])
+@pytest.mark.unittest
+def test_validate_url_source(mock_page_key_to_ini, source, expected_error):
+    '''The test url produces the expected error, or no error as may be
+    '''
+    errors = []
+    checkset.validate_url_source(errors, None, source)
+
+    if expected_error:
+        assert len(errors) == 1
+        assert isinstance(errors[0], expected_error)
+    else:
+        assert len(errors) == 0
+
+
+mock_validate_url_source = testing.make_mock_fixture(
+    checkset, 'validate_url_source')
+
+
+# validate_url_path()
+
+@pytest.mark.parametrize(
+    ('path',), [
+        ('',),
+        ('foo',)])
+@pytest.mark.unittest
+def test_validate_url_path_no_slash(mock_page_key_to_ini, path):
+    '''When the path does not begin with a /,
+    the right error is added to errors
+    '''
+    errors = []
+    checkset.validate_url_path(errors, 'ignored', {'url_path': path})
+
+    assert len(errors) == 1
+    assert isinstance(errors[0], common_ex.BadFileURLPathError)
+
+
+@pytest.mark.parametrize(
+    ('path',), [
+        ('/',),
+        ('/foo',)])
+@pytest.mark.unittest
+def test_validate_url_path_slash(mock_page_key_to_ini, path):
+    '''When the path begins with a '/',  no error is added to errors
+    '''
+    errors = []
+    checkset.validate_url_path(errors, 'ignored', {'url_path': path})
+
+    assert len(errors) == 0
+
+
+mock_validate_url_path = testing.make_mock_fixture(
+    checkset, 'validate_url_path')
+
+
+# validate_file_source()
+
+@pytest.mark.parametrize(
+    ('source',), [
+        ('',),
+        ('foo',)])
+@pytest.mark.unittest
+def test_validate_file_source_no_slash(mock_page_key_to_ini, source):
+    '''When the source does not begin with a /,
+    the right error is added to errors
+    '''
+    errors = []
+    checkset.validate_file_source(errors, 'ignored', source)
+
+    assert len(errors) == 1
+    assert isinstance(errors[0], common_ex.BadFileSourceError)
+
+
+@pytest.mark.parametrize(
+    ('source',), [
+        ('/',),
+        ('/foo',)])
+@pytest.mark.unittest
+def test_validate_file_source_slash(mock_page_key_to_ini, source):
+    '''When the source begins with a '/',  no error is added to errors
+    '''
+    errors = []
+    checkset.validate_file_source(errors, 'ignored', source)
+
+    assert len(errors) == 0
+
+
+mock_validate_file_source = testing.make_mock_fixture(
+    checkset, 'validate_file_source')
+
+
+# validate_route_source()
+
+@pytest.mark.unittest
+def test_validate_route_source_empty(mock_page_key_to_ini):
+    '''When there is no source the right error is added to errors
+    '''
+    errors = []
+    checkset.validate_route_source(errors, 'ignored', '')
+
+    assert len(errors) == 1
+    assert isinstance(errors[0], common_ex.BadRouteSourceError)
+
+
+@pytest.mark.unittest
+def test_validate_route_source_not_empty(mock_page_key_to_ini):
+    '''When there is a source no error is added to errors
+    '''
+    errors = []
+    checkset.validate_route_source(errors, 'ignored', 'something')
+
+    assert len(errors) == 0
+
+
+mock_validate_route_source = testing.make_mock_fixture(
+    checkset, 'validate_route_source')
+
+
+# validate_asset_source()
+
+@pytest.mark.unittest
+def test_validate_asset_source_empty(mock_page_key_to_ini):
+    '''When there is no source the right error is added to errors
+    '''
+    errors = []
+    checkset.validate_asset_source(errors, 'ignored', '')
+
+    assert len(errors) == 1
+    assert isinstance(errors[0], common_ex.BadAssetSourceError)
+
+
+@pytest.mark.unittest
+def test_validate_asset_source_not_empty(mock_page_key_to_ini):
+    '''When there is a source no error is added to errors
+    '''
+    errors = []
+    checkset.validate_asset_source(errors, 'ignored', 'something')
+
+    assert len(errors) == 0
+
+
+mock_validate_asset_source = testing.make_mock_fixture(
+    checkset, 'validate_asset_source')
+
+
+# validate_file_content()
+
+@pytest.mark.parametrize(
+    ('have_settings', 'vup_called'), [
+        (True, 1),
+        (False, 0)])
+@pytest.mark.unittest
+def test_validate_file_content(
+        mock_validate_file_source, mock_require_page_settings,
+        mock_validate_url_path,
+        mock_unknown_settings, have_settings, vup_called):
+    '''validate_file_source() is called, validate_url_path()
+    is called when settings validate, the unknown_settings()
+    return value is appended to the errors
+    '''
+    expected_errors = ['some error']
+    mock_require_page_settings.return_value = have_settings
+    mock_unknown_settings.return_value = expected_errors
+
+    errors = []
+    checkset.validate_file_content(errors, None, None, None)
+
+    mock_validate_file_source.assert_called_once()
+    mock_require_page_settings.assert_called_once()
+    assert mock_validate_url_path.call_count == vup_called
+    assert errors == expected_errors
+
+
+mock_validate_file_content = testing.make_mock_fixture(
+    checkset, 'validate_file_content')
+
+
+# validate_type_content()
+
+@pytest.mark.parametrize(
+    ('page_settings',
+     'vus_called',
+     'vfc_called',
+     'vrs_called',
+     'vas_called',
+     'pkti_called',
+     'error_class'), [
+         # URL type
+         ({'type': 'URL',
+           'source': 'ignored'},
+          1, 0, 0, 0, 0,
+          common_ex.UnknownSettingKeyError),
+         # file type
+         ({'type': 'file',
+           'source': 'ignored'},
+          0, 1, 0, 0, 0,
+          common_ex.MissingSettingError),
+         # route type
+         ({'type': 'route',
+           'source': 'ignored'},
+          0, 0, 1, 0, 0,
+          common_ex.UnknownSettingKeyError),
+         # asset type
+         ({'type': 'asset',
+           'source': 'ignored'},
+          0, 0, 0, 1, 0,
+          common_ex.UnknownSettingKeyError),
+         # a unknown type
+         ({'type': 'unknown',
+           'source': 'ignored'},
+          0, 0, 0, 0, 1,
+          common_ex.BadPageTypeError)])
+@pytest.mark.unittest
+def test_validate_type_content(
+        mock_validate_url_source, mock_unknown_settings,
+        mock_validate_file_content, mock_validate_route_source,
+        mock_validate_asset_source, mock_page_key_to_ini,
+        page_settings, vus_called, vfc_called,
+        vrs_called, vas_called, pkti_called, error_class):
+    '''The expected calls are make, the expected errors returned
+    '''
+    mock_validate_file_content.side_effect = (
+        lambda errors, *args:
+        errors.append(common_ex.MissingSettingError('ignored')))
+    mock_unknown_settings.return_value = [common_ex.UnknownSettingKeyError(
+        'ignored')]
+
+    errors = []
+    checkset.validate_type_content(errors, 'some_page', page_settings)
+
+    assert mock_validate_url_source.call_count == vus_called
+    assert mock_validate_file_content.call_count == vfc_called
+    assert mock_validate_asset_source.call_count == vas_called
+    assert mock_validate_route_source.call_count == vrs_called
+    assert len(errors) == 1
+    assert isinstance(errors[0], error_class)
+
+
+mock_validate_type_content = testing.make_mock_fixture(
+    checkset, 'validate_type_content')
+
+
+# validate_page_setting()
+
+@pytest.mark.unittest
+def test_validate_page_setting_nopage(
+        mock_require_page_settings, mock_validate_type_content):
+    '''When the page does not have a setting, nothing is done
+    '''
+    errors = []
+    settings = {'pgwui': {}}
+    result = checkset.validate_page_setting(errors, settings, 'test_page')
+
+    assert errors == []
+    assert result is None
+    mock_require_page_settings.assert_not_called()
+    mock_validate_type_content.assert_not_called()
+
+
+@pytest.mark.unittest
+def test_validate_page_setting_not_required(
+        mock_require_page_settings, mock_validate_type_content):
+    '''When require_page_settings() says something is missing, nothing is done
+    '''
+    errors = []
+    settings = {'pgwui': {'test_page': 'ignored'}}
+    mock_require_page_settings.return_value = False
+    result = checkset.validate_page_setting(errors, settings, 'test_page')
+
+    assert errors == []
+    assert result is None
+    mock_require_page_settings.assert_called_once()
+    mock_validate_type_content.assert_not_called()
+
+
+@pytest.mark.unittest
+def test_validate_page_setting_required(
+        mock_require_page_settings, mock_validate_type_content):
+    '''When require_page_settings() says nothing is missing,
+    validate_type_content() is called
+    '''
+    errors = []
+    settings = {'pgwui': {'test_page': 'ignored'}}
+    mock_require_page_settings.return_value = True
+    result = checkset.validate_page_setting(errors, settings, 'test_page')
+
+    assert errors == []
+    assert result is None
+    mock_require_page_settings.assert_called_once()
+    mock_validate_type_content.assert_called_once()
index eb958ef9b1e82c0828bf22c374468aee4a980872..db125e0b662531b2de97ffa18ba29550fe1a46c8 100644 (file)
@@ -78,10 +78,10 @@ mock_validate_hmac = testing.make_mock_fixture(
 
 # Unit tests
 
-# dot_to_component_settings()
+# dot_to_multiline_setting()
 
 @pytest.mark.unittest
-def test_dot_to_component_settings_new():
+def test_dot_to_multiline_setting_new(mock_parse_assignments):
     '''Adds a new dict and puts the settings in it
     '''
     comp_settings = {'foo': 'foo', 'bar': 'bar'}
@@ -91,14 +91,15 @@ def test_dot_to_component_settings_new():
                 key: comp_settings}
     expected = {'pgwui': {component: comp_settings}}
 
-    pgwui_server.dot_to_component_settings(
+    mock_parse_assignments.return_value = comp_settings
+    pgwui_server.dot_to_multiline_setting(
         settings, key, 'pgwui_component')
 
     assert settings == expected
 
 
 @pytest.mark.unittest
-def test_dot_to_component_settings_old():
+def test_dot_to_multiline_setting_old(mock_parse_assignments):
     '''Extends an existing dict in the settings
     '''
     comp_settings = {'foo': 'foo', 'bar': 'bar'}
@@ -109,21 +110,22 @@ def test_dot_to_component_settings_old():
     expected = {'pgwui':
                 {component: {'foo': 'foo', 'bar': 'bar', 'baz': 'baz'}}}
 
-    pgwui_server.dot_to_component_settings(
+    mock_parse_assignments.return_value = comp_settings
+    pgwui_server.dot_to_multiline_setting(
         settings, key, 'pgwui_component')
 
     assert settings == expected
 
 
-mock_dot_to_component_setting = testing.make_mock_fixture(
-    pgwui_server, 'dot_to_component_settings')
+mock_dot_to_multiline_setting = testing.make_mock_fixture(
+    pgwui_server, 'dot_to_multiline_setting')
 
 
 # component_setting_into_dict()
 
 @pytest.mark.unittest
 def test_component_setting_into_dict_no_checker(
-        mock_dot_to_component_setting):
+        mock_dot_to_multiline_setting):
     '''When there's no checker nothing is done
     '''
     errors = []
@@ -136,7 +138,7 @@ def test_component_setting_into_dict_no_checker(
 
 @pytest.mark.unittest
 def test_component_setting_into_dict_checker(
-        mock_dot_to_component_setting):
+        mock_dot_to_multiline_setting):
     '''When there's a checker its result is appended to the errors
     '''
     errors = ['someerror']
@@ -240,9 +242,9 @@ mock_parse_assignments = testing.make_mock_fixture(
 
 @pytest.mark.unittest
 def test_setting_into_dict_unknown(
-        mock_parse_assignments,
         mock_component_setting_into_dict,
-        mock_dot_to_dict):
+        mock_dot_to_dict,
+        mock_dot_to_multiline_setting):
     '''No new errors when there's a non-pgwui setting'''
     errors = []
     pgwui_server.setting_into_dict(errors, [], {}, 'foo', {})
@@ -254,7 +256,8 @@ def test_setting_into_dict_unknown(
 def test_setting_into_dict_bad(
         mock_parse_assignments,
         mock_component_setting_into_dict,
-        mock_dot_to_dict):
+        mock_dot_to_dict,
+        mock_dot_to_multiline_setting):
     '''Delivers an error on a bad pgwui setting'''
     errors = []
 
@@ -267,24 +270,44 @@ def test_setting_into_dict_bad(
 
 @pytest.mark.unittest
 def test_setting_into_dict_good(
-        mock_parse_assignments,
         mock_component_setting_into_dict,
-        mock_dot_to_dict):
-    '''Calls dot_to_dict when a known pgwui setting is supplied'''
+        mock_dot_to_dict,
+        mock_dot_to_multiline_setting):
+    '''Calls dot_to_dict when a known pgwui setting is supplied
+    '''
     errors = []
 
     pgwui_server.setting_into_dict(
         errors, [], {}, 'pgwui.pg_host', {})
 
     mock_dot_to_dict.assert_called_once()
+    mock_dot_to_multiline_setting.assert_not_called()
+    assert errors == []
+
+
+@pytest.mark.unittest
+def test_setting_into_dict_multiline(
+        mock_component_setting_into_dict,
+        mock_dot_to_dict,
+        mock_dot_to_multiline_setting):
+    '''Calls dot_to_multiline_setting when a known pgwui multi-line
+    setting is supplied
+    '''
+    errors = []
+
+    pgwui_server.setting_into_dict(
+        errors, [], {}, 'pgwui.home_page', {})
+
+    mock_dot_to_dict.assert_not_called()
+    mock_dot_to_multiline_setting.assert_called_once()
     assert errors == []
 
 
 @pytest.mark.unittest
 def test_setting_into_dict_plugin_component(
-        mock_parse_assignments,
         mock_component_setting_into_dict,
-        mock_dot_to_dict):
+        mock_dot_to_dict,
+        mock_dot_to_multiline_setting):
     '''When a setting is for a component the setting is parsed and
     moved into a dict
     '''
@@ -296,7 +319,6 @@ def test_setting_into_dict_plugin_component(
     pgwui_server.setting_into_dict(
         errors, ['pgwui_component'], {}, key, settings)
 
-    mock_parse_assignments.assert_called_once()
     mock_component_setting_into_dict.assert_called_once()
     assert errors == []
 
@@ -395,27 +417,48 @@ mock_exit_reporting_errors = testing.make_mock_fixture(
     pgwui_server, 'exit_reporting_errors')
 
 
+# add_default_settings()
+
+@pytest.mark.unittest
+def test_add_default_settings():
+    '''The default settings are added
+    '''
+    settings = dict()
+    pgwui_server.add_default_settings(settings)
+
+    assert settings == pgwui_server.DEFAULT_SETTINGS
+
+
+mock_add_default_settings = testing.make_mock_fixture(
+    pgwui_server, 'add_default_settings')
+
+
 # exit_on_invalid_settings()
 
 @pytest.mark.unittest
-def test_exit_on_invalid_settings_invalid(monkeypatch,
-                                          mock_exit_reporting_errors):
+def test_exit_on_invalid_settings_invalid(
+        monkeypatch,
+        mock_add_default_settings, mock_dictify_settings,
+        mock_exit_reporting_errors):
     '''Calls dictify_settings and exit_reporting_errors() when
     setting is invalid
     '''
-    def mock_dictify_settings(errors, settings, components):
+    def mymock(errors, settings, components):
         errors.append('error1')
 
-    monkeypatch.setattr(pgwui_server, 'dictify_settings',
-                        mock_dictify_settings)
+    mock_dictify_settings.side_effect = mymock
 
     pgwui_server.exit_on_invalid_settings({}, [])
 
+    mock_dictify_settings.assert_called_once()
+    mock_add_default_settings.assert_called_once()
     assert mock_exit_reporting_errors.called
 
 
 @pytest.mark.unittest
-def test_exit_on_invalid_settings_valid(mock_dictify_settings):
+def test_exit_on_invalid_settings_valid(
+        mock_add_default_settings, mock_dictify_settings,
+        mock_exit_reporting_errors):
     '''Returns, without exiting, when all settings are valid
     '''
     pgwui_server.exit_on_invalid_settings({}, [])
@@ -502,13 +545,33 @@ def test_add_routes_notempty(mock_add_route, mock_parse_assignments):
     assert mocked_add_route.call_count == len(test_routes)
 
 
+@pytest.mark.unittest
+def test_add_routes_menu(mock_add_route, mock_parse_assignments, caplog):
+    '''When there is a a route for pgwui_menu, but there is a menu_page
+    setting, no route is added and an INFO message is logged
+    '''
+    caplog.set_level(logging.DEBUG)
+
+    test_routes = [('pgwui_menu', 'notused')]
+    mock_parse_assignments.return_value = test_routes
+    with pyramid.testing.testConfig() as config:
+        mocked_add_route = mock_add_route(config)
+        pgwui_server.add_routes(config, {'pgwui': {'routes': 'notused',
+                                                   'menu_page': 'anything'}})
+
+    mocked_add_route.assert_not_called()
+
+    logs = caplog.record_tuples
+    assert len(logs) == 1
+    assert logs[0][1] == logging.INFO
+
+
 mock_add_routes = testing.make_mock_fixture(
     pgwui_server, 'add_routes')
 
 
 # apply_component_defaults()
 
-
 @pytest.mark.unittest
 def test_apply_component_defaults(monkeypatch, caplog,
                                   mock_autoconfigurable_components,