add basic support for menu handler
default menu is not yet useful, but the handler mostly works. except for actual auth/perm checks since we have no users yet!
This commit is contained in:
parent
001179c87f
commit
60e8303d29
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
app
|
app
|
||||||
helpers
|
helpers
|
||||||
|
menus
|
||||||
static
|
static
|
||||||
subscribers
|
subscribers
|
||||||
util
|
util
|
||||||
|
|
6
docs/api/wuttaweb/menus.rst
Normal file
6
docs/api/wuttaweb/menus.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
``wuttaweb.menus``
|
||||||
|
==================
|
||||||
|
|
||||||
|
.. automodule:: wuttaweb.menus
|
||||||
|
:members:
|
|
@ -48,6 +48,10 @@ tests = ["pytest-cov", "tox"]
|
||||||
main = "wuttaweb.app:main"
|
main = "wuttaweb.app:main"
|
||||||
|
|
||||||
|
|
||||||
|
[project.entry-points."wutta.providers"]
|
||||||
|
wuttaweb = "wuttaweb.app:WebAppProvider"
|
||||||
|
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://rattailproject.org/"
|
Homepage = "https://rattailproject.org/"
|
||||||
Repository = "https://kallithea.rattailproject.org/rattail-project/wuttaweb"
|
Repository = "https://kallithea.rattailproject.org/rattail-project/wuttaweb"
|
||||||
|
|
|
@ -26,11 +26,38 @@ Application
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from wuttjamaican.app import AppProvider
|
||||||
from wuttjamaican.conf import make_config
|
from wuttjamaican.conf import make_config
|
||||||
|
|
||||||
from pyramid.config import Configurator
|
from pyramid.config import Configurator
|
||||||
|
|
||||||
|
|
||||||
|
class WebAppProvider(AppProvider):
|
||||||
|
"""
|
||||||
|
The :term:`app provider<app provider>` for WuttaWeb. This adds
|
||||||
|
some methods to get web-specific :term:`handlers<handler>`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_web_menu_handler(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Get the configured "menu" handler for the web app.
|
||||||
|
|
||||||
|
Specify a custom handler in your config file like this:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[wuttaweb]
|
||||||
|
menus.handler_spec = poser.web.menus:PoserMenuHandler
|
||||||
|
|
||||||
|
:returns: Instance of :class:`~wuttaweb.menus.MenuHandler`.
|
||||||
|
"""
|
||||||
|
if 'web_menu_handler' not in self.__dict__:
|
||||||
|
spec = self.config.get('wuttaweb.menus.handler_spec',
|
||||||
|
default='wuttaweb.menus:MenuHandler')
|
||||||
|
self.web_menu_handler = self.app.load_object(spec)(self.config)
|
||||||
|
return self.web_menu_handler
|
||||||
|
|
||||||
|
|
||||||
def make_wutta_config(settings):
|
def make_wutta_config(settings):
|
||||||
"""
|
"""
|
||||||
Make a WuttaConfig object from the given settings.
|
Make a WuttaConfig object from the given settings.
|
||||||
|
|
297
src/wuttaweb/menus.py
Normal file
297
src/wuttaweb/menus.py
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# wuttaweb -- Web App for Wutta Framework
|
||||||
|
# Copyright © 2024 Lance Edgar
|
||||||
|
#
|
||||||
|
# This file is part of Wutta Framework.
|
||||||
|
#
|
||||||
|
# Wutta Framework is free software: you can redistribute it and/or modify it
|
||||||
|
# under the terms of the GNU General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
# Wutta Framework 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 General Public License for
|
||||||
|
# more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Main Menu
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from wuttjamaican.app import GenericHandler
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MenuHandler(GenericHandler):
|
||||||
|
"""
|
||||||
|
Base class and default implementation for menu handler.
|
||||||
|
|
||||||
|
It is assumed that most apps will override the menu handler with
|
||||||
|
their own subclass. In particular the subclass will override
|
||||||
|
:meth:`make_menus()` and/or :meth:`make_admin_menu()`.
|
||||||
|
|
||||||
|
The app should normally not instantiate the menu handler directly,
|
||||||
|
but instead call
|
||||||
|
:meth:`~wuttaweb.app.WebAppProvider.get_web_menu_handler()` on the
|
||||||
|
:term:`app handler`.
|
||||||
|
|
||||||
|
To configure your menu handler to be used, do this within your
|
||||||
|
:term:`config extension`::
|
||||||
|
|
||||||
|
config.setdefault('wuttaweb.menus.handler_spec', 'poser.web.menus:PoserMenuHandler')
|
||||||
|
|
||||||
|
The core web app will call :meth:`do_make_menus()` to get the
|
||||||
|
final (possibly filtered) menu set for the current user. The
|
||||||
|
menu set should be a list of dicts, for example::
|
||||||
|
|
||||||
|
menus = [
|
||||||
|
{
|
||||||
|
'title': "First Dropdown",
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'title': "Foo",
|
||||||
|
'route': 'foo',
|
||||||
|
},
|
||||||
|
{'type': 'sep'}, # horizontal line
|
||||||
|
{
|
||||||
|
'title': "Bar",
|
||||||
|
'route': 'bar',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': "Second Dropdown",
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'title': "Wikipedia",
|
||||||
|
'url': 'https://en.wikipedia.org',
|
||||||
|
'target': '_blank',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# default menu definitions
|
||||||
|
##############################
|
||||||
|
|
||||||
|
def make_menus(self, request, **kwargs):
|
||||||
|
"""
|
||||||
|
Generate the full set of menus for the app.
|
||||||
|
|
||||||
|
This method provides a semi-sane menu set by default, but it
|
||||||
|
is expected for most apps to override it.
|
||||||
|
|
||||||
|
The return value should be a list of dicts as described above.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
self.make_admin_menu(request),
|
||||||
|
]
|
||||||
|
|
||||||
|
def make_admin_menu(self, request, **kwargs):
|
||||||
|
"""
|
||||||
|
Generate a typical Admin menu.
|
||||||
|
|
||||||
|
This method provides a semi-sane menu set by default, but it
|
||||||
|
is expected for most apps to override it.
|
||||||
|
|
||||||
|
The return value for this method should be a *single* dict,
|
||||||
|
which will ultimately be one element of the final list of
|
||||||
|
dicts as described above.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'title': "Admin",
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'title': "TODO!",
|
||||||
|
'url': '#',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# default internal logic
|
||||||
|
##############################
|
||||||
|
|
||||||
|
def do_make_menus(self, request, **kwargs):
|
||||||
|
"""
|
||||||
|
This method is responsible for constructing the final menu
|
||||||
|
set. It first calls :meth:`make_menus()` to get the basic
|
||||||
|
set, and then it prunes entries as needed based on current
|
||||||
|
user permissions.
|
||||||
|
|
||||||
|
The web app calls this method but you normally should not need
|
||||||
|
to override it; you can override :meth:`make_menus()` instead.
|
||||||
|
"""
|
||||||
|
raw_menus = self.make_menus(request, **kwargs)
|
||||||
|
|
||||||
|
# now we have "simple" (raw) menus definition, but must refine
|
||||||
|
# that somewhat to produce our final menus
|
||||||
|
self._mark_allowed(request, raw_menus)
|
||||||
|
final_menus = []
|
||||||
|
for topitem in raw_menus:
|
||||||
|
|
||||||
|
if topitem['allowed']:
|
||||||
|
|
||||||
|
if topitem.get('type') == 'link':
|
||||||
|
final_menus.append(self._make_menu_entry(request, topitem))
|
||||||
|
|
||||||
|
else: # assuming 'menu' type
|
||||||
|
|
||||||
|
menu_items = []
|
||||||
|
for item in topitem['items']:
|
||||||
|
if not item['allowed']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# nested submenu
|
||||||
|
if item.get('type') == 'menu':
|
||||||
|
submenu_items = []
|
||||||
|
for subitem in item['items']:
|
||||||
|
if subitem['allowed']:
|
||||||
|
submenu_items.append(self._make_menu_entry(request, subitem))
|
||||||
|
menu_items.append({
|
||||||
|
'type': 'submenu',
|
||||||
|
'title': item['title'],
|
||||||
|
'items': submenu_items,
|
||||||
|
'is_menu': True,
|
||||||
|
'is_sep': False,
|
||||||
|
})
|
||||||
|
|
||||||
|
elif item.get('type') == 'sep':
|
||||||
|
# we only want to add a sep, *if* we already have some
|
||||||
|
# menu items (i.e. there is something to separate)
|
||||||
|
# *and* the last menu item is not a sep (avoid doubles)
|
||||||
|
if menu_items and not menu_items[-1]['is_sep']:
|
||||||
|
menu_items.append(self._make_menu_entry(request, item))
|
||||||
|
|
||||||
|
else: # standard menu item
|
||||||
|
menu_items.append(self._make_menu_entry(request, item))
|
||||||
|
|
||||||
|
# remove final separator if present
|
||||||
|
if menu_items and menu_items[-1]['is_sep']:
|
||||||
|
menu_items.pop()
|
||||||
|
|
||||||
|
# only add if we wound up with something
|
||||||
|
assert menu_items
|
||||||
|
if menu_items:
|
||||||
|
group = {
|
||||||
|
'type': 'menu',
|
||||||
|
'key': topitem.get('key'),
|
||||||
|
'title': topitem['title'],
|
||||||
|
'items': menu_items,
|
||||||
|
'is_menu': True,
|
||||||
|
'is_link': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# topitem w/ no key likely means it did not come
|
||||||
|
# from config but rather explicit definition in
|
||||||
|
# code. so we are free to "invent" a (safe) key
|
||||||
|
# for it, since that is only for editing config
|
||||||
|
if not group['key']:
|
||||||
|
group['key'] = self._make_menu_key(topitem['title'])
|
||||||
|
|
||||||
|
final_menus.append(group)
|
||||||
|
|
||||||
|
return final_menus
|
||||||
|
|
||||||
|
def _is_allowed(self, request, item):
|
||||||
|
"""
|
||||||
|
Logic to determine if a given menu item is "allowed" for
|
||||||
|
current user.
|
||||||
|
"""
|
||||||
|
perm = item.get('perm')
|
||||||
|
# TODO
|
||||||
|
# if perm:
|
||||||
|
# return request.has_perm(perm)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _mark_allowed(self, request, menus):
|
||||||
|
"""
|
||||||
|
Traverse the menu set, and mark each item as "allowed" (or
|
||||||
|
not) based on current user permissions.
|
||||||
|
"""
|
||||||
|
for topitem in menus:
|
||||||
|
|
||||||
|
if topitem.get('type', 'menu') == 'link':
|
||||||
|
topitem['allowed'] = True
|
||||||
|
|
||||||
|
elif topitem.get('type', 'menu') == 'menu':
|
||||||
|
topitem['allowed'] = False
|
||||||
|
|
||||||
|
for item in topitem['items']:
|
||||||
|
|
||||||
|
if item.get('type') == 'menu':
|
||||||
|
for subitem in item['items']:
|
||||||
|
subitem['allowed'] = self._is_allowed(request, subitem)
|
||||||
|
|
||||||
|
item['allowed'] = False
|
||||||
|
for subitem in item['items']:
|
||||||
|
if subitem['allowed'] and subitem.get('type') != 'sep':
|
||||||
|
item['allowed'] = True
|
||||||
|
break
|
||||||
|
|
||||||
|
else:
|
||||||
|
item['allowed'] = self._is_allowed(request, item)
|
||||||
|
|
||||||
|
for item in topitem['items']:
|
||||||
|
if item['allowed'] and item.get('type') != 'sep':
|
||||||
|
topitem['allowed'] = True
|
||||||
|
break
|
||||||
|
|
||||||
|
def _make_menu_entry(self, request, item):
|
||||||
|
"""
|
||||||
|
Convert a simple menu entry dict, into a proper menu-related
|
||||||
|
object, for use in constructing final menu.
|
||||||
|
"""
|
||||||
|
# separator
|
||||||
|
if item.get('type') == 'sep':
|
||||||
|
return {
|
||||||
|
'type': 'sep',
|
||||||
|
'is_menu': False,
|
||||||
|
'is_sep': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# standard menu item
|
||||||
|
entry = {
|
||||||
|
'type': 'item',
|
||||||
|
'title': item['title'],
|
||||||
|
'perm': item.get('perm'),
|
||||||
|
'target': item.get('target'),
|
||||||
|
'is_link': True,
|
||||||
|
'is_menu': False,
|
||||||
|
'is_sep': False,
|
||||||
|
}
|
||||||
|
if item.get('route'):
|
||||||
|
entry['route'] = item['route']
|
||||||
|
try:
|
||||||
|
entry['url'] = request.route_url(entry['route'])
|
||||||
|
except KeyError: # happens if no such route
|
||||||
|
log.warning("invalid route name for menu entry: %s", entry)
|
||||||
|
entry['url'] = entry['route']
|
||||||
|
entry['key'] = entry['route']
|
||||||
|
else:
|
||||||
|
if item.get('url'):
|
||||||
|
entry['url'] = item['url']
|
||||||
|
entry['key'] = self._make_menu_key(entry['title'])
|
||||||
|
return entry
|
||||||
|
|
||||||
|
def _make_menu_key(self, value):
|
||||||
|
"""
|
||||||
|
Generate a normalized menu key for the given value.
|
||||||
|
"""
|
||||||
|
return re.sub(r'\W', '', value.lower())
|
|
@ -115,6 +115,12 @@ def before_render(event):
|
||||||
|
|
||||||
Reference to the built-in module, :mod:`python:json`.
|
Reference to the built-in module, :mod:`python:json`.
|
||||||
|
|
||||||
|
.. data:: 'menus'
|
||||||
|
|
||||||
|
Set of entries to be shown in the main menu. This is obtained
|
||||||
|
by calling :meth:`~wuttaweb.menus.MenuHandler.do_make_menus()`
|
||||||
|
on the configured :class:`~wuttaweb.menus.MenuHandler`.
|
||||||
|
|
||||||
.. data:: 'url'
|
.. data:: 'url'
|
||||||
|
|
||||||
Reference to the request method,
|
Reference to the request method,
|
||||||
|
@ -131,8 +137,8 @@ def before_render(event):
|
||||||
context['url'] = request.route_url
|
context['url'] = request.route_url
|
||||||
context['json'] = json
|
context['json'] = json
|
||||||
|
|
||||||
# TODO
|
menus = app.get_web_menu_handler()
|
||||||
context['menus'] = []
|
context['menus'] = menus.do_make_menus(request)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
|
@ -103,6 +103,11 @@
|
||||||
}
|
}
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
/* nb. this refers to the "menu-sized" app title in far left of main menu */
|
||||||
|
#global-header-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
#current-context {
|
#current-context {
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,6 @@
|
||||||
|
|
||||||
<%def name="footer()">
|
<%def name="footer()">
|
||||||
<p class="has-text-centered">
|
<p class="has-text-centered">
|
||||||
powered by ${h.link_to("Wutta Framework", 'https://pypi.org/project/WuttJamaican/', target='_blank')}
|
powered by ${h.link_to("WuttaWeb", 'https://wuttaproject.org/', target='_blank')}
|
||||||
</p>
|
</p>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
315
tests/test_menus.py
Normal file
315
tests/test_menus.py
Normal file
|
@ -0,0 +1,315 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
from wuttjamaican.conf import WuttaConfig
|
||||||
|
|
||||||
|
from pyramid import testing
|
||||||
|
|
||||||
|
from wuttaweb import menus as mod
|
||||||
|
|
||||||
|
|
||||||
|
class TestMenuHandler(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.config = WuttaConfig()
|
||||||
|
self.app = self.config.get_app()
|
||||||
|
self.handler = mod.MenuHandler(self.config)
|
||||||
|
self.request = testing.DummyRequest()
|
||||||
|
|
||||||
|
def test_make_admin_menu(self):
|
||||||
|
menus = self.handler.make_admin_menu(self.request)
|
||||||
|
self.assertIsInstance(menus, dict)
|
||||||
|
|
||||||
|
def test_make_menus(self):
|
||||||
|
menus = self.handler.make_menus(self.request)
|
||||||
|
self.assertIsInstance(menus, list)
|
||||||
|
|
||||||
|
def test_is_allowed(self):
|
||||||
|
# TODO: this should test auth/perm handling
|
||||||
|
item = {}
|
||||||
|
self.assertTrue(self.handler._is_allowed(self.request, item))
|
||||||
|
|
||||||
|
def test_mark_allowed(self):
|
||||||
|
|
||||||
|
def make_menus():
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{'title': "Foo", 'url': '#'},
|
||||||
|
{'title': "Bar", 'url': '#'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
mock_is_allowed = MagicMock()
|
||||||
|
with patch.object(self.handler, '_is_allowed', new=mock_is_allowed):
|
||||||
|
|
||||||
|
# all should be allowed
|
||||||
|
mock_is_allowed.return_value = True
|
||||||
|
menus = make_menus()
|
||||||
|
self.handler._mark_allowed(self.request, menus)
|
||||||
|
menu = menus[0]
|
||||||
|
self.assertTrue(menu['allowed'])
|
||||||
|
foo, bar = menu['items']
|
||||||
|
self.assertTrue(foo['allowed'])
|
||||||
|
self.assertTrue(bar['allowed'])
|
||||||
|
|
||||||
|
# none should be allowed
|
||||||
|
mock_is_allowed.return_value = False
|
||||||
|
menus = make_menus()
|
||||||
|
self.handler._mark_allowed(self.request, menus)
|
||||||
|
menu = menus[0]
|
||||||
|
self.assertFalse(menu['allowed'])
|
||||||
|
foo, bar = menu['items']
|
||||||
|
self.assertFalse(foo['allowed'])
|
||||||
|
self.assertFalse(bar['allowed'])
|
||||||
|
|
||||||
|
def test_mark_allowed_submenu(self):
|
||||||
|
|
||||||
|
def make_menus():
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{'title': "Foo", 'url': '#'},
|
||||||
|
{
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{'title': "Bar", 'url': '#'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
mock_is_allowed = MagicMock()
|
||||||
|
with patch.object(self.handler, '_is_allowed', new=mock_is_allowed):
|
||||||
|
|
||||||
|
# all should be allowed
|
||||||
|
mock_is_allowed.return_value = True
|
||||||
|
menus = make_menus()
|
||||||
|
self.handler._mark_allowed(self.request, menus)
|
||||||
|
menu = menus[0]
|
||||||
|
self.assertTrue(menu['allowed'])
|
||||||
|
foo, submenu = menu['items']
|
||||||
|
self.assertTrue(foo['allowed'])
|
||||||
|
self.assertTrue(submenu['allowed'])
|
||||||
|
subitem = submenu['items'][0]
|
||||||
|
self.assertTrue(subitem['allowed'])
|
||||||
|
|
||||||
|
# none should be allowed
|
||||||
|
mock_is_allowed.return_value = False
|
||||||
|
menus = make_menus()
|
||||||
|
self.handler._mark_allowed(self.request, menus)
|
||||||
|
menu = menus[0]
|
||||||
|
self.assertFalse(menu['allowed'])
|
||||||
|
foo, submenu = menu['items']
|
||||||
|
self.assertFalse(foo['allowed'])
|
||||||
|
self.assertFalse(submenu['allowed'])
|
||||||
|
subitem = submenu['items'][0]
|
||||||
|
self.assertFalse(subitem['allowed'])
|
||||||
|
|
||||||
|
def test_make_menu_key(self):
|
||||||
|
self.assertEqual(self.handler._make_menu_key('foo'), 'foo')
|
||||||
|
self.assertEqual(self.handler._make_menu_key('FooBar'), 'foobar')
|
||||||
|
self.assertEqual(self.handler._make_menu_key('Foo - $#Bar'), 'foobar')
|
||||||
|
self.assertEqual(self.handler._make_menu_key('Foo__Bar'), 'foo__bar')
|
||||||
|
|
||||||
|
def test_make_menu_entry_item(self):
|
||||||
|
item = {'title': "Foo", 'url': '#'}
|
||||||
|
entry = self.handler._make_menu_entry(self.request, item)
|
||||||
|
self.assertEqual(entry['type'], 'item')
|
||||||
|
self.assertEqual(entry['title'], "Foo")
|
||||||
|
self.assertEqual(entry['url'], '#')
|
||||||
|
self.assertTrue(entry['is_link'])
|
||||||
|
|
||||||
|
def test_make_menu_entry_item_with_no_url(self):
|
||||||
|
item = {'title': "Foo"}
|
||||||
|
entry = self.handler._make_menu_entry(self.request, item)
|
||||||
|
self.assertEqual(entry['type'], 'item')
|
||||||
|
self.assertEqual(entry['title'], "Foo")
|
||||||
|
self.assertNotIn('url', entry)
|
||||||
|
# nb. still sets is_link = True; basically it's <a> with no href
|
||||||
|
self.assertTrue(entry['is_link'])
|
||||||
|
|
||||||
|
def test_make_menu_entry_item_with_known_route(self):
|
||||||
|
item = {'title': "Foo", 'route': 'home'}
|
||||||
|
with patch.object(self.request, 'route_url', return_value='/something'):
|
||||||
|
entry = self.handler._make_menu_entry(self.request, item)
|
||||||
|
self.assertEqual(entry['type'], 'item')
|
||||||
|
self.assertEqual(entry['url'], '/something')
|
||||||
|
self.assertTrue(entry['is_link'])
|
||||||
|
|
||||||
|
def test_make_menu_entry_item_with_unknown_route(self):
|
||||||
|
item = {'title': "Foo", 'route': 'home'}
|
||||||
|
with patch.object(self.request, 'route_url', side_effect=KeyError):
|
||||||
|
entry = self.handler._make_menu_entry(self.request, item)
|
||||||
|
self.assertEqual(entry['type'], 'item')
|
||||||
|
# nb. fake url is used, based on (bad) route name
|
||||||
|
self.assertEqual(entry['url'], 'home')
|
||||||
|
self.assertTrue(entry['is_link'])
|
||||||
|
|
||||||
|
def test_make_menu_entry_sep(self):
|
||||||
|
item = {'type': 'sep'}
|
||||||
|
entry = self.handler._make_menu_entry(self.request, item)
|
||||||
|
self.assertEqual(entry['type'], 'sep')
|
||||||
|
self.assertTrue(entry['is_sep'])
|
||||||
|
self.assertFalse(entry['is_menu'])
|
||||||
|
|
||||||
|
def test_do_make_menus_prune_unallowed_item(self):
|
||||||
|
test_menus = [
|
||||||
|
{
|
||||||
|
'title': "First Menu",
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{'title': "Foo", 'url': '#'},
|
||||||
|
{'title': "Bar", 'url': '#'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
def is_allowed(request, item):
|
||||||
|
if item.get('title') == 'Bar':
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
with patch.object(self.handler, 'make_menus', return_value=test_menus):
|
||||||
|
with patch.object(self.handler, '_is_allowed', side_effect=is_allowed):
|
||||||
|
menus = self.handler.do_make_menus(self.request)
|
||||||
|
|
||||||
|
# Foo remains but Bar is pruned
|
||||||
|
menu = menus[0]
|
||||||
|
self.assertEqual(len(menu['items']), 1)
|
||||||
|
item = menu['items'][0]
|
||||||
|
self.assertEqual(item['title'], 'Foo')
|
||||||
|
|
||||||
|
def test_do_make_menus_prune_unallowed_menu(self):
|
||||||
|
test_menus = [
|
||||||
|
{
|
||||||
|
'title': "First Menu",
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{'title': "Foo", 'url': '#'},
|
||||||
|
{'title': "Bar", 'url': '#'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': "Second Menu",
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{'title': "Baz", 'url': '#'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
def is_allowed(request, item):
|
||||||
|
if item.get('title') == 'Baz':
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
with patch.object(self.handler, 'make_menus', return_value=test_menus):
|
||||||
|
with patch.object(self.handler, '_is_allowed', side_effect=is_allowed):
|
||||||
|
menus = self.handler.do_make_menus(self.request)
|
||||||
|
|
||||||
|
# Second/Baz remains but First/Foo/Bar are pruned
|
||||||
|
self.assertEqual(len(menus), 1)
|
||||||
|
menu = menus[0]
|
||||||
|
self.assertEqual(menu['title'], 'Second Menu')
|
||||||
|
self.assertEqual(len(menu['items']), 1)
|
||||||
|
item = menu['items'][0]
|
||||||
|
self.assertEqual(item['title'], 'Baz')
|
||||||
|
|
||||||
|
def test_do_make_menus_with_top_link(self):
|
||||||
|
test_menus = [
|
||||||
|
{
|
||||||
|
'title': "First Menu",
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{'title': "Foo", 'url': '#'},
|
||||||
|
{'title': "Bar", 'url': '#'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': "Second Link",
|
||||||
|
'type': 'link',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch.object(self.handler, 'make_menus', return_value=test_menus):
|
||||||
|
with patch.object(self.handler, '_is_allowed', return_value=True):
|
||||||
|
menus = self.handler.do_make_menus(self.request)
|
||||||
|
|
||||||
|
# ensure top link remains
|
||||||
|
self.assertEqual(len(menus), 2)
|
||||||
|
menu = menus[1]
|
||||||
|
self.assertEqual(menu['title'], "Second Link")
|
||||||
|
|
||||||
|
def test_do_make_menus_with_trailing_sep(self):
|
||||||
|
test_menus = [
|
||||||
|
{
|
||||||
|
'title': "First Menu",
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{'title': "Foo", 'url': '#'},
|
||||||
|
{'title': "Bar", 'url': '#'},
|
||||||
|
{'type': 'sep'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch.object(self.handler, 'make_menus', return_value=test_menus):
|
||||||
|
with patch.object(self.handler, '_is_allowed', return_value=True):
|
||||||
|
menus = self.handler.do_make_menus(self.request)
|
||||||
|
|
||||||
|
# ensure trailing sep was pruned
|
||||||
|
menu = menus[0]
|
||||||
|
self.assertEqual(len(menu['items']), 2)
|
||||||
|
foo, bar = menu['items']
|
||||||
|
self.assertEqual(foo['title'], 'Foo')
|
||||||
|
self.assertEqual(bar['title'], 'Bar')
|
||||||
|
|
||||||
|
def test_do_make_menus_with_submenu(self):
|
||||||
|
test_menus = [
|
||||||
|
{
|
||||||
|
'title': "First Menu",
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'title': "First Submenu",
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{'title': "Foo", 'url': '#'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': "Second Submenu",
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{'title': "Bar", 'url': '#'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
def is_allowed(request, item):
|
||||||
|
if item.get('title') == 'Bar':
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
with patch.object(self.handler, 'make_menus', return_value=test_menus):
|
||||||
|
with patch.object(self.handler, '_is_allowed', side_effect=is_allowed):
|
||||||
|
menus = self.handler.do_make_menus(self.request)
|
||||||
|
|
||||||
|
# first submenu remains, second is pruned
|
||||||
|
menu = menus[0]
|
||||||
|
self.assertEqual(len(menu['items']), 1)
|
||||||
|
submenu = menu['items'][0]
|
||||||
|
self.assertEqual(submenu['type'], 'submenu')
|
||||||
|
self.assertEqual(submenu['title'], 'First Submenu')
|
||||||
|
self.assertEqual(len(submenu['items']), 1)
|
||||||
|
item = submenu['items'][0]
|
||||||
|
self.assertEqual(item['title'], 'Foo')
|
Loading…
Reference in a new issue