WuttaWeb 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/CHANGELOG.md +8 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/PKG-INFO +7 -9
- wuttaweb-0.1.0/README.rst → wuttaweb-0.2.0/README.md +1 -3
- wuttaweb-0.2.0/docs/api/wuttaweb/handler.rst +6 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/api/wuttaweb/index.rst +2 -0
- wuttaweb-0.2.0/docs/api/wuttaweb/menus.rst +6 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/index.rst +5 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/pyproject.toml +10 -6
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/app.py +27 -0
- wuttaweb-0.2.0/src/wuttaweb/handler.py +57 -0
- wuttaweb-0.2.0/src/wuttaweb/menus.py +307 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/subscribers.py +13 -2
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/templates/base.mako +5 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/templates/base_meta.mako +1 -1
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/tasks.py +1 -1
- wuttaweb-0.2.0/tests/test_handler.py +20 -0
- wuttaweb-0.2.0/tests/test_menus.py +321 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/.gitignore +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/COPYING.txt +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/Makefile +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/_static/.keepme +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/api/index.rst +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/api/wuttaweb/app.rst +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/api/wuttaweb/helpers.rst +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/api/wuttaweb/static.rst +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/api/wuttaweb/subscribers.rst +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/api/wuttaweb/util.rst +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/api/wuttaweb/views.base.rst +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/api/wuttaweb/views.common.rst +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/api/wuttaweb/views.rst +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/conf.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/glossary.rst +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/make.bat +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/docs/narr/index.rst +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/__init__.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/_version.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/helpers.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/static/__init__.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/static/img/testing.png +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/templates/home.mako +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/templates/page.mako +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/util.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/views/__init__.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/views/base.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/src/wuttaweb/views/common.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/tests/__init__.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/tests/test_app.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/tests/test_helpers.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/tests/test_static.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/tests/test_subscribers.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/tests/test_util.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/tests/views/__init__.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/tests/views/test___init__.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/tests/views/test_base.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/tests/views/test_common.py +0 -0
- {wuttaweb-0.1.0 → wuttaweb-0.2.0}/tox.ini +0 -0
|
@@ -5,6 +5,14 @@ All notable changes to wuttaweb will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## v0.2.0 (2024-07-14)
|
|
9
|
+
|
|
10
|
+
### Feat
|
|
11
|
+
|
|
12
|
+
- add basic support for menu handler
|
|
13
|
+
|
|
14
|
+
- add "web handler" feature; it must get the menu handler
|
|
15
|
+
|
|
8
16
|
## v0.1.0 (2024-07-12)
|
|
9
17
|
|
|
10
18
|
### Feat
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: WuttaWeb
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Web App for Wutta Framework
|
|
5
|
-
Project-URL: Homepage, https://
|
|
6
|
-
Project-URL: Repository, https://
|
|
7
|
-
Project-URL: Changelog, https://
|
|
5
|
+
Project-URL: Homepage, https://wuttaproject.org/
|
|
6
|
+
Project-URL: Repository, https://forgejo.wuttaproject.org/wutta/wuttaweb
|
|
7
|
+
Project-URL: Changelog, https://forgejo.wuttaproject.org/wutta/wuttaweb/src/branch/master/CHANGELOG.md
|
|
8
8
|
Author-email: Lance Edgar <lance@edbob.org>
|
|
9
9
|
License: GNU GPL v3+
|
|
10
10
|
License-File: COPYING.txt
|
|
@@ -29,19 +29,17 @@ Requires-Dist: pyramid-mako
|
|
|
29
29
|
Requires-Dist: pyramid>=2
|
|
30
30
|
Requires-Dist: waitress
|
|
31
31
|
Requires-Dist: webhelpers2
|
|
32
|
-
Requires-Dist: wuttjamaican[db]>=0.
|
|
32
|
+
Requires-Dist: wuttjamaican[db]>=0.7.0
|
|
33
33
|
Provides-Extra: docs
|
|
34
34
|
Requires-Dist: furo; extra == 'docs'
|
|
35
35
|
Requires-Dist: sphinx; extra == 'docs'
|
|
36
36
|
Provides-Extra: tests
|
|
37
37
|
Requires-Dist: pytest-cov; extra == 'tests'
|
|
38
38
|
Requires-Dist: tox; extra == 'tests'
|
|
39
|
-
Description-Content-Type: text/
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
wuttaweb
|
|
44
|
-
==========
|
|
42
|
+
# wuttaweb
|
|
45
43
|
|
|
46
44
|
Web app for Wutta Framework
|
|
47
45
|
|
|
@@ -6,6 +6,11 @@ This package provides a "web layer" for custom apps.
|
|
|
6
6
|
|
|
7
7
|
It uses traditional server-side rendering with VueJS on the front-end.
|
|
8
8
|
|
|
9
|
+
Good documentation and 100% `test coverage`_ are priorities for this
|
|
10
|
+
project.
|
|
11
|
+
|
|
12
|
+
.. _test coverage: https://buildbot.rattailproject.org/coverage/wuttaweb/
|
|
13
|
+
|
|
9
14
|
.. toctree::
|
|
10
15
|
:maxdepth: 3
|
|
11
16
|
:caption: Contents:
|
|
@@ -6,9 +6,9 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "WuttaWeb"
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.2.0"
|
|
10
10
|
description = "Web App for Wutta Framework"
|
|
11
|
-
readme = "README.
|
|
11
|
+
readme = "README.md"
|
|
12
12
|
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
|
13
13
|
license = {text = "GNU GPL v3+"}
|
|
14
14
|
classifiers = [
|
|
@@ -35,7 +35,7 @@ dependencies = [
|
|
|
35
35
|
"pyramid_mako",
|
|
36
36
|
"waitress",
|
|
37
37
|
"WebHelpers2",
|
|
38
|
-
"WuttJamaican[db]>=0.
|
|
38
|
+
"WuttJamaican[db]>=0.7.0",
|
|
39
39
|
]
|
|
40
40
|
|
|
41
41
|
|
|
@@ -48,10 +48,14 @@ tests = ["pytest-cov", "tox"]
|
|
|
48
48
|
main = "wuttaweb.app:main"
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
[project.entry-points."wutta.app.providers"]
|
|
52
|
+
wuttaweb = "wuttaweb.app:WebAppProvider"
|
|
53
|
+
|
|
54
|
+
|
|
51
55
|
[project.urls]
|
|
52
|
-
Homepage = "https://
|
|
53
|
-
Repository = "https://
|
|
54
|
-
Changelog = "https://
|
|
56
|
+
Homepage = "https://wuttaproject.org/"
|
|
57
|
+
Repository = "https://forgejo.wuttaproject.org/wutta/wuttaweb"
|
|
58
|
+
Changelog = "https://forgejo.wuttaproject.org/wutta/wuttaweb/src/branch/master/CHANGELOG.md"
|
|
55
59
|
|
|
56
60
|
|
|
57
61
|
[tool.commitizen]
|
|
@@ -26,11 +26,38 @@ Application
|
|
|
26
26
|
|
|
27
27
|
import os
|
|
28
28
|
|
|
29
|
+
from wuttjamaican.app import AppProvider
|
|
29
30
|
from wuttjamaican.conf import make_config
|
|
30
31
|
|
|
31
32
|
from pyramid.config import Configurator
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
class WebAppProvider(AppProvider):
|
|
36
|
+
"""
|
|
37
|
+
The :term:`app provider` for WuttaWeb. This adds some methods
|
|
38
|
+
specific to web apps.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def get_web_handler(self, **kwargs):
|
|
42
|
+
"""
|
|
43
|
+
Get the configured "web" handler for the app.
|
|
44
|
+
|
|
45
|
+
Specify a custom handler in your config file like this:
|
|
46
|
+
|
|
47
|
+
.. code-block:: ini
|
|
48
|
+
|
|
49
|
+
[wutta]
|
|
50
|
+
web.handler_spec = poser.web.handler:PoserWebHandler
|
|
51
|
+
|
|
52
|
+
:returns: Instance of :class:`~wuttaweb.handler.WebHandler`.
|
|
53
|
+
"""
|
|
54
|
+
if 'web_handler' not in self.__dict__:
|
|
55
|
+
spec = self.config.get(f'{self.appname}.web.handler_spec',
|
|
56
|
+
default='wuttaweb.handler:WebHandler')
|
|
57
|
+
self.web_handler = self.app.load_object(spec)(self.config)
|
|
58
|
+
return self.web_handler
|
|
59
|
+
|
|
60
|
+
|
|
34
61
|
def make_wutta_config(settings):
|
|
35
62
|
"""
|
|
36
63
|
Make a WuttaConfig object from the given settings.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# -*- coding: utf-8; -*-
|
|
2
|
+
################################################################################
|
|
3
|
+
#
|
|
4
|
+
# wuttaweb -- Web App for Wutta Framework
|
|
5
|
+
# Copyright © 2024 Lance Edgar
|
|
6
|
+
#
|
|
7
|
+
# This file is part of Wutta Framework.
|
|
8
|
+
#
|
|
9
|
+
# Wutta Framework is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU General Public License as published by the Free
|
|
11
|
+
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
12
|
+
# later version.
|
|
13
|
+
#
|
|
14
|
+
# Wutta Framework is distributed in the hope that it will be useful, but
|
|
15
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
17
|
+
# more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License along with
|
|
20
|
+
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
################################################################################
|
|
23
|
+
"""
|
|
24
|
+
Web Handler
|
|
25
|
+
|
|
26
|
+
This defines the :term:`handler` for the web layer.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from wuttjamaican.app import GenericHandler
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WebHandler(GenericHandler):
|
|
33
|
+
"""
|
|
34
|
+
Base class and default implementation for the "web" :term:`handler`.
|
|
35
|
+
|
|
36
|
+
This is responsible for determining the "menu handler" and
|
|
37
|
+
(eventually) possibly other things.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def get_menu_handler(self, **kwargs):
|
|
41
|
+
"""
|
|
42
|
+
Get the configured "menu" handler for the web app.
|
|
43
|
+
|
|
44
|
+
Specify a custom handler in your config file like this:
|
|
45
|
+
|
|
46
|
+
.. code-block:: ini
|
|
47
|
+
|
|
48
|
+
[wutta.web]
|
|
49
|
+
menus.handler_spec = poser.web.menus:PoserMenuHandler
|
|
50
|
+
|
|
51
|
+
:returns: Instance of :class:`~wuttaweb.menus.MenuHandler`.
|
|
52
|
+
"""
|
|
53
|
+
if not hasattr(self, 'menu_handler'):
|
|
54
|
+
spec = self.config.get(f'{self.appname}.web.menus.handler_spec',
|
|
55
|
+
default='wuttaweb.menus:MenuHandler')
|
|
56
|
+
self.menu_handler = self.app.load_object(spec)(self.config)
|
|
57
|
+
return self.menu_handler
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# -*- coding: utf-8; -*-
|
|
2
|
+
################################################################################
|
|
3
|
+
#
|
|
4
|
+
# wuttaweb -- Web App for Wutta Framework
|
|
5
|
+
# Copyright © 2024 Lance Edgar
|
|
6
|
+
#
|
|
7
|
+
# This file is part of Wutta Framework.
|
|
8
|
+
#
|
|
9
|
+
# Wutta Framework is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU General Public License as published by the Free
|
|
11
|
+
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
12
|
+
# later version.
|
|
13
|
+
#
|
|
14
|
+
# Wutta Framework is distributed in the hope that it will be useful, but
|
|
15
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
17
|
+
# more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License along with
|
|
20
|
+
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
################################################################################
|
|
23
|
+
"""
|
|
24
|
+
Main Menu
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
import logging
|
|
29
|
+
|
|
30
|
+
from wuttjamaican.app import GenericHandler
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
log = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MenuHandler(GenericHandler):
|
|
37
|
+
"""
|
|
38
|
+
Base class and default implementation for menu handler.
|
|
39
|
+
|
|
40
|
+
It is assumed that most apps will override the menu handler with
|
|
41
|
+
their own subclass. In particular the subclass will override
|
|
42
|
+
:meth:`make_menus()` and/or :meth:`make_admin_menu()`.
|
|
43
|
+
|
|
44
|
+
The app should normally not instantiate the menu handler directly,
|
|
45
|
+
but instead call
|
|
46
|
+
:meth:`~wuttaweb.app.WebAppProvider.get_web_menu_handler()` on the
|
|
47
|
+
:term:`app handler`.
|
|
48
|
+
|
|
49
|
+
To configure your menu handler to be used, do this within your
|
|
50
|
+
:term:`config extension`::
|
|
51
|
+
|
|
52
|
+
config.setdefault('wuttaweb.menus.handler_spec', 'poser.web.menus:PoserMenuHandler')
|
|
53
|
+
|
|
54
|
+
The core web app will call :meth:`do_make_menus()` to get the
|
|
55
|
+
final (possibly filtered) menu set for the current user. The
|
|
56
|
+
menu set should be a list of dicts, for example::
|
|
57
|
+
|
|
58
|
+
menus = [
|
|
59
|
+
{
|
|
60
|
+
'title': "First Dropdown",
|
|
61
|
+
'type': 'menu',
|
|
62
|
+
'items': [
|
|
63
|
+
{
|
|
64
|
+
'title': "Foo",
|
|
65
|
+
'route': 'foo',
|
|
66
|
+
},
|
|
67
|
+
{'type': 'sep'}, # horizontal line
|
|
68
|
+
{
|
|
69
|
+
'title': "Bar",
|
|
70
|
+
'route': 'bar',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
'title': "Second Dropdown",
|
|
76
|
+
'type': 'menu',
|
|
77
|
+
'items': [
|
|
78
|
+
{
|
|
79
|
+
'title': "Wikipedia",
|
|
80
|
+
'url': 'https://en.wikipedia.org',
|
|
81
|
+
'target': '_blank',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
]
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
##############################
|
|
89
|
+
# default menu definitions
|
|
90
|
+
##############################
|
|
91
|
+
|
|
92
|
+
def make_menus(self, request, **kwargs):
|
|
93
|
+
"""
|
|
94
|
+
Generate the full set of menus for the app.
|
|
95
|
+
|
|
96
|
+
This method provides a semi-sane menu set by default, but it
|
|
97
|
+
is expected for most apps to override it.
|
|
98
|
+
|
|
99
|
+
The return value should be a list of dicts as described above.
|
|
100
|
+
"""
|
|
101
|
+
return [
|
|
102
|
+
self.make_admin_menu(request),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
def make_admin_menu(self, request, **kwargs):
|
|
106
|
+
"""
|
|
107
|
+
Generate a typical Admin menu.
|
|
108
|
+
|
|
109
|
+
This method provides a semi-sane menu set by default, but it
|
|
110
|
+
is expected for most apps to override it.
|
|
111
|
+
|
|
112
|
+
The return value for this method should be a *single* dict,
|
|
113
|
+
which will ultimately be one element of the final list of
|
|
114
|
+
dicts as described above.
|
|
115
|
+
"""
|
|
116
|
+
return {
|
|
117
|
+
'title': "Admin",
|
|
118
|
+
'type': 'menu',
|
|
119
|
+
'items': [
|
|
120
|
+
{
|
|
121
|
+
'title': "TODO!",
|
|
122
|
+
'url': '#',
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
##############################
|
|
128
|
+
# default internal logic
|
|
129
|
+
##############################
|
|
130
|
+
|
|
131
|
+
def do_make_menus(self, request, **kwargs):
|
|
132
|
+
"""
|
|
133
|
+
This method is responsible for constructing the final menu
|
|
134
|
+
set. It first calls :meth:`make_menus()` to get the basic
|
|
135
|
+
set, and then it prunes entries as needed based on current
|
|
136
|
+
user permissions.
|
|
137
|
+
|
|
138
|
+
The web app calls this method but you normally should not need
|
|
139
|
+
to override it; you can override :meth:`make_menus()` instead.
|
|
140
|
+
"""
|
|
141
|
+
raw_menus = self._make_raw_menus(request, **kwargs)
|
|
142
|
+
|
|
143
|
+
# now we have "simple" (raw) menus definition, but must refine
|
|
144
|
+
# that somewhat to produce our final menus
|
|
145
|
+
self._mark_allowed(request, raw_menus)
|
|
146
|
+
final_menus = []
|
|
147
|
+
for topitem in raw_menus:
|
|
148
|
+
|
|
149
|
+
if topitem['allowed']:
|
|
150
|
+
|
|
151
|
+
if topitem.get('type') == 'link':
|
|
152
|
+
final_menus.append(self._make_menu_entry(request, topitem))
|
|
153
|
+
|
|
154
|
+
else: # assuming 'menu' type
|
|
155
|
+
|
|
156
|
+
menu_items = []
|
|
157
|
+
for item in topitem['items']:
|
|
158
|
+
if not item['allowed']:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
# nested submenu
|
|
162
|
+
if item.get('type') == 'menu':
|
|
163
|
+
submenu_items = []
|
|
164
|
+
for subitem in item['items']:
|
|
165
|
+
if subitem['allowed']:
|
|
166
|
+
submenu_items.append(self._make_menu_entry(request, subitem))
|
|
167
|
+
menu_items.append({
|
|
168
|
+
'type': 'submenu',
|
|
169
|
+
'title': item['title'],
|
|
170
|
+
'items': submenu_items,
|
|
171
|
+
'is_menu': True,
|
|
172
|
+
'is_sep': False,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
elif item.get('type') == 'sep':
|
|
176
|
+
# we only want to add a sep, *if* we already have some
|
|
177
|
+
# menu items (i.e. there is something to separate)
|
|
178
|
+
# *and* the last menu item is not a sep (avoid doubles)
|
|
179
|
+
if menu_items and not menu_items[-1]['is_sep']:
|
|
180
|
+
menu_items.append(self._make_menu_entry(request, item))
|
|
181
|
+
|
|
182
|
+
else: # standard menu item
|
|
183
|
+
menu_items.append(self._make_menu_entry(request, item))
|
|
184
|
+
|
|
185
|
+
# remove final separator if present
|
|
186
|
+
if menu_items and menu_items[-1]['is_sep']:
|
|
187
|
+
menu_items.pop()
|
|
188
|
+
|
|
189
|
+
# only add if we wound up with something
|
|
190
|
+
assert menu_items
|
|
191
|
+
if menu_items:
|
|
192
|
+
group = {
|
|
193
|
+
'type': 'menu',
|
|
194
|
+
'key': topitem.get('key'),
|
|
195
|
+
'title': topitem['title'],
|
|
196
|
+
'items': menu_items,
|
|
197
|
+
'is_menu': True,
|
|
198
|
+
'is_link': False,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# topitem w/ no key likely means it did not come
|
|
202
|
+
# from config but rather explicit definition in
|
|
203
|
+
# code. so we are free to "invent" a (safe) key
|
|
204
|
+
# for it, since that is only for editing config
|
|
205
|
+
if not group['key']:
|
|
206
|
+
group['key'] = self._make_menu_key(topitem['title'])
|
|
207
|
+
|
|
208
|
+
final_menus.append(group)
|
|
209
|
+
|
|
210
|
+
return final_menus
|
|
211
|
+
|
|
212
|
+
def _make_raw_menus(self, request, **kwargs):
|
|
213
|
+
"""
|
|
214
|
+
Construct the initial full set of "raw" menus.
|
|
215
|
+
|
|
216
|
+
For now this just calls :meth:`make_menus()` which generally
|
|
217
|
+
means a "hard-coded" menu set. Eventually it may allow for
|
|
218
|
+
loading dynamic menus from config instead.
|
|
219
|
+
"""
|
|
220
|
+
return self.make_menus(request, **kwargs)
|
|
221
|
+
|
|
222
|
+
def _is_allowed(self, request, item):
|
|
223
|
+
"""
|
|
224
|
+
Logic to determine if a given menu item is "allowed" for
|
|
225
|
+
current user.
|
|
226
|
+
"""
|
|
227
|
+
perm = item.get('perm')
|
|
228
|
+
# TODO
|
|
229
|
+
# if perm:
|
|
230
|
+
# return request.has_perm(perm)
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
def _mark_allowed(self, request, menus):
|
|
234
|
+
"""
|
|
235
|
+
Traverse the menu set, and mark each item as "allowed" (or
|
|
236
|
+
not) based on current user permissions.
|
|
237
|
+
"""
|
|
238
|
+
for topitem in menus:
|
|
239
|
+
|
|
240
|
+
if topitem.get('type', 'menu') == 'link':
|
|
241
|
+
topitem['allowed'] = True
|
|
242
|
+
|
|
243
|
+
elif topitem.get('type', 'menu') == 'menu':
|
|
244
|
+
topitem['allowed'] = False
|
|
245
|
+
|
|
246
|
+
for item in topitem['items']:
|
|
247
|
+
|
|
248
|
+
if item.get('type') == 'menu':
|
|
249
|
+
for subitem in item['items']:
|
|
250
|
+
subitem['allowed'] = self._is_allowed(request, subitem)
|
|
251
|
+
|
|
252
|
+
item['allowed'] = False
|
|
253
|
+
for subitem in item['items']:
|
|
254
|
+
if subitem['allowed'] and subitem.get('type') != 'sep':
|
|
255
|
+
item['allowed'] = True
|
|
256
|
+
break
|
|
257
|
+
|
|
258
|
+
else:
|
|
259
|
+
item['allowed'] = self._is_allowed(request, item)
|
|
260
|
+
|
|
261
|
+
for item in topitem['items']:
|
|
262
|
+
if item['allowed'] and item.get('type') != 'sep':
|
|
263
|
+
topitem['allowed'] = True
|
|
264
|
+
break
|
|
265
|
+
|
|
266
|
+
def _make_menu_entry(self, request, item):
|
|
267
|
+
"""
|
|
268
|
+
Convert a simple menu entry dict, into a proper menu-related
|
|
269
|
+
object, for use in constructing final menu.
|
|
270
|
+
"""
|
|
271
|
+
# separator
|
|
272
|
+
if item.get('type') == 'sep':
|
|
273
|
+
return {
|
|
274
|
+
'type': 'sep',
|
|
275
|
+
'is_menu': False,
|
|
276
|
+
'is_sep': True,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# standard menu item
|
|
280
|
+
entry = {
|
|
281
|
+
'type': 'item',
|
|
282
|
+
'title': item['title'],
|
|
283
|
+
'perm': item.get('perm'),
|
|
284
|
+
'target': item.get('target'),
|
|
285
|
+
'is_link': True,
|
|
286
|
+
'is_menu': False,
|
|
287
|
+
'is_sep': False,
|
|
288
|
+
}
|
|
289
|
+
if item.get('route'):
|
|
290
|
+
entry['route'] = item['route']
|
|
291
|
+
try:
|
|
292
|
+
entry['url'] = request.route_url(entry['route'])
|
|
293
|
+
except KeyError: # happens if no such route
|
|
294
|
+
log.warning("invalid route name for menu entry: %s", entry)
|
|
295
|
+
entry['url'] = entry['route']
|
|
296
|
+
entry['key'] = entry['route']
|
|
297
|
+
else:
|
|
298
|
+
if item.get('url'):
|
|
299
|
+
entry['url'] = item['url']
|
|
300
|
+
entry['key'] = self._make_menu_key(entry['title'])
|
|
301
|
+
return entry
|
|
302
|
+
|
|
303
|
+
def _make_menu_key(self, value):
|
|
304
|
+
"""
|
|
305
|
+
Generate a normalized menu key for the given value.
|
|
306
|
+
"""
|
|
307
|
+
return re.sub(r'\W', '', value.lower())
|
|
@@ -36,12 +36,16 @@ hooks contained here, depending on the circumstance.
|
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
38
|
import json
|
|
39
|
+
import logging
|
|
39
40
|
|
|
40
41
|
from pyramid import threadlocal
|
|
41
42
|
|
|
42
43
|
from wuttaweb import helpers
|
|
43
44
|
|
|
44
45
|
|
|
46
|
+
log = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
45
49
|
def new_request(event):
|
|
46
50
|
"""
|
|
47
51
|
Event hook called when processing a new request.
|
|
@@ -115,6 +119,12 @@ def before_render(event):
|
|
|
115
119
|
|
|
116
120
|
Reference to the built-in module, :mod:`python:json`.
|
|
117
121
|
|
|
122
|
+
.. data:: 'menus'
|
|
123
|
+
|
|
124
|
+
Set of entries to be shown in the main menu. This is obtained
|
|
125
|
+
by calling :meth:`~wuttaweb.menus.MenuHandler.do_make_menus()`
|
|
126
|
+
on the configured :class:`~wuttaweb.menus.MenuHandler`.
|
|
127
|
+
|
|
118
128
|
.. data:: 'url'
|
|
119
129
|
|
|
120
130
|
Reference to the request method,
|
|
@@ -123,6 +133,7 @@ def before_render(event):
|
|
|
123
133
|
request = event.get('request') or threadlocal.get_current_request()
|
|
124
134
|
config = request.wutta_config
|
|
125
135
|
app = config.get_app()
|
|
136
|
+
web = app.get_web_handler()
|
|
126
137
|
|
|
127
138
|
context = event
|
|
128
139
|
context['app'] = app
|
|
@@ -131,8 +142,8 @@ def before_render(event):
|
|
|
131
142
|
context['url'] = request.route_url
|
|
132
143
|
context['json'] = json
|
|
133
144
|
|
|
134
|
-
|
|
135
|
-
context['menus'] =
|
|
145
|
+
menus = web.get_menu_handler()
|
|
146
|
+
context['menus'] = menus.do_make_menus(request)
|
|
136
147
|
|
|
137
148
|
|
|
138
149
|
def includeme(config):
|
|
@@ -16,6 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
<%def name="footer()">
|
|
18
18
|
<p class="has-text-centered">
|
|
19
|
-
powered by ${h.link_to("
|
|
19
|
+
powered by ${h.link_to("WuttaWeb", 'https://wuttaproject.org/', target='_blank')}
|
|
20
20
|
</p>
|
|
21
21
|
</%def>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# -*- coding: utf-8; -*-
|
|
2
|
+
|
|
3
|
+
from unittest import TestCase
|
|
4
|
+
|
|
5
|
+
from wuttjamaican.conf import WuttaConfig
|
|
6
|
+
|
|
7
|
+
from wuttaweb import handler as mod
|
|
8
|
+
from wuttaweb.menus import MenuHandler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestWebHandler(TestCase):
|
|
12
|
+
|
|
13
|
+
def setUp(self):
|
|
14
|
+
self.config = WuttaConfig()
|
|
15
|
+
self.app = self.config.get_app()
|
|
16
|
+
self.handler = mod.WebHandler(self.config)
|
|
17
|
+
|
|
18
|
+
def test_menu_handler_default(self):
|
|
19
|
+
menus = self.handler.get_menu_handler()
|
|
20
|
+
self.assertIsInstance(menus, MenuHandler)
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# -*- coding: utf-8; -*-
|
|
2
|
+
|
|
3
|
+
from unittest import TestCase
|
|
4
|
+
from unittest.mock import patch, MagicMock
|
|
5
|
+
|
|
6
|
+
from wuttjamaican.conf import WuttaConfig
|
|
7
|
+
|
|
8
|
+
from pyramid import testing
|
|
9
|
+
|
|
10
|
+
from wuttaweb import menus as mod
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestMenuHandler(TestCase):
|
|
14
|
+
|
|
15
|
+
def setUp(self):
|
|
16
|
+
self.config = WuttaConfig()
|
|
17
|
+
self.app = self.config.get_app()
|
|
18
|
+
self.handler = mod.MenuHandler(self.config)
|
|
19
|
+
self.request = testing.DummyRequest()
|
|
20
|
+
|
|
21
|
+
def test_make_admin_menu(self):
|
|
22
|
+
menus = self.handler.make_admin_menu(self.request)
|
|
23
|
+
self.assertIsInstance(menus, dict)
|
|
24
|
+
|
|
25
|
+
def test_make_menus(self):
|
|
26
|
+
menus = self.handler.make_menus(self.request)
|
|
27
|
+
self.assertIsInstance(menus, list)
|
|
28
|
+
|
|
29
|
+
def test_is_allowed(self):
|
|
30
|
+
# TODO: this should test auth/perm handling
|
|
31
|
+
item = {}
|
|
32
|
+
self.assertTrue(self.handler._is_allowed(self.request, item))
|
|
33
|
+
|
|
34
|
+
def test_mark_allowed(self):
|
|
35
|
+
|
|
36
|
+
def make_menus():
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
'type': 'menu',
|
|
40
|
+
'items': [
|
|
41
|
+
{'title': "Foo", 'url': '#'},
|
|
42
|
+
{'title': "Bar", 'url': '#'},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
mock_is_allowed = MagicMock()
|
|
48
|
+
with patch.object(self.handler, '_is_allowed', new=mock_is_allowed):
|
|
49
|
+
|
|
50
|
+
# all should be allowed
|
|
51
|
+
mock_is_allowed.return_value = True
|
|
52
|
+
menus = make_menus()
|
|
53
|
+
self.handler._mark_allowed(self.request, menus)
|
|
54
|
+
menu = menus[0]
|
|
55
|
+
self.assertTrue(menu['allowed'])
|
|
56
|
+
foo, bar = menu['items']
|
|
57
|
+
self.assertTrue(foo['allowed'])
|
|
58
|
+
self.assertTrue(bar['allowed'])
|
|
59
|
+
|
|
60
|
+
# none should be allowed
|
|
61
|
+
mock_is_allowed.return_value = False
|
|
62
|
+
menus = make_menus()
|
|
63
|
+
self.handler._mark_allowed(self.request, menus)
|
|
64
|
+
menu = menus[0]
|
|
65
|
+
self.assertFalse(menu['allowed'])
|
|
66
|
+
foo, bar = menu['items']
|
|
67
|
+
self.assertFalse(foo['allowed'])
|
|
68
|
+
self.assertFalse(bar['allowed'])
|
|
69
|
+
|
|
70
|
+
def test_mark_allowed_submenu(self):
|
|
71
|
+
|
|
72
|
+
def make_menus():
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
'type': 'menu',
|
|
76
|
+
'items': [
|
|
77
|
+
{'title': "Foo", 'url': '#'},
|
|
78
|
+
{
|
|
79
|
+
'type': 'menu',
|
|
80
|
+
'items': [
|
|
81
|
+
{'title': "Bar", 'url': '#'},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
mock_is_allowed = MagicMock()
|
|
89
|
+
with patch.object(self.handler, '_is_allowed', new=mock_is_allowed):
|
|
90
|
+
|
|
91
|
+
# all should be allowed
|
|
92
|
+
mock_is_allowed.return_value = True
|
|
93
|
+
menus = make_menus()
|
|
94
|
+
self.handler._mark_allowed(self.request, menus)
|
|
95
|
+
menu = menus[0]
|
|
96
|
+
self.assertTrue(menu['allowed'])
|
|
97
|
+
foo, submenu = menu['items']
|
|
98
|
+
self.assertTrue(foo['allowed'])
|
|
99
|
+
self.assertTrue(submenu['allowed'])
|
|
100
|
+
subitem = submenu['items'][0]
|
|
101
|
+
self.assertTrue(subitem['allowed'])
|
|
102
|
+
|
|
103
|
+
# none should be allowed
|
|
104
|
+
mock_is_allowed.return_value = False
|
|
105
|
+
menus = make_menus()
|
|
106
|
+
self.handler._mark_allowed(self.request, menus)
|
|
107
|
+
menu = menus[0]
|
|
108
|
+
self.assertFalse(menu['allowed'])
|
|
109
|
+
foo, submenu = menu['items']
|
|
110
|
+
self.assertFalse(foo['allowed'])
|
|
111
|
+
self.assertFalse(submenu['allowed'])
|
|
112
|
+
subitem = submenu['items'][0]
|
|
113
|
+
self.assertFalse(subitem['allowed'])
|
|
114
|
+
|
|
115
|
+
def test_make_menu_key(self):
|
|
116
|
+
self.assertEqual(self.handler._make_menu_key('foo'), 'foo')
|
|
117
|
+
self.assertEqual(self.handler._make_menu_key('FooBar'), 'foobar')
|
|
118
|
+
self.assertEqual(self.handler._make_menu_key('Foo - $#Bar'), 'foobar')
|
|
119
|
+
self.assertEqual(self.handler._make_menu_key('Foo__Bar'), 'foo__bar')
|
|
120
|
+
|
|
121
|
+
def test_make_menu_entry_item(self):
|
|
122
|
+
item = {'title': "Foo", 'url': '#'}
|
|
123
|
+
entry = self.handler._make_menu_entry(self.request, item)
|
|
124
|
+
self.assertEqual(entry['type'], 'item')
|
|
125
|
+
self.assertEqual(entry['title'], "Foo")
|
|
126
|
+
self.assertEqual(entry['url'], '#')
|
|
127
|
+
self.assertTrue(entry['is_link'])
|
|
128
|
+
|
|
129
|
+
def test_make_menu_entry_item_with_no_url(self):
|
|
130
|
+
item = {'title': "Foo"}
|
|
131
|
+
entry = self.handler._make_menu_entry(self.request, item)
|
|
132
|
+
self.assertEqual(entry['type'], 'item')
|
|
133
|
+
self.assertEqual(entry['title'], "Foo")
|
|
134
|
+
self.assertNotIn('url', entry)
|
|
135
|
+
# nb. still sets is_link = True; basically it's <a> with no href
|
|
136
|
+
self.assertTrue(entry['is_link'])
|
|
137
|
+
|
|
138
|
+
def test_make_menu_entry_item_with_known_route(self):
|
|
139
|
+
item = {'title': "Foo", 'route': 'home'}
|
|
140
|
+
with patch.object(self.request, 'route_url', return_value='/something'):
|
|
141
|
+
entry = self.handler._make_menu_entry(self.request, item)
|
|
142
|
+
self.assertEqual(entry['type'], 'item')
|
|
143
|
+
self.assertEqual(entry['url'], '/something')
|
|
144
|
+
self.assertTrue(entry['is_link'])
|
|
145
|
+
|
|
146
|
+
def test_make_menu_entry_item_with_unknown_route(self):
|
|
147
|
+
item = {'title': "Foo", 'route': 'home'}
|
|
148
|
+
with patch.object(self.request, 'route_url', side_effect=KeyError):
|
|
149
|
+
entry = self.handler._make_menu_entry(self.request, item)
|
|
150
|
+
self.assertEqual(entry['type'], 'item')
|
|
151
|
+
# nb. fake url is used, based on (bad) route name
|
|
152
|
+
self.assertEqual(entry['url'], 'home')
|
|
153
|
+
self.assertTrue(entry['is_link'])
|
|
154
|
+
|
|
155
|
+
def test_make_menu_entry_sep(self):
|
|
156
|
+
item = {'type': 'sep'}
|
|
157
|
+
entry = self.handler._make_menu_entry(self.request, item)
|
|
158
|
+
self.assertEqual(entry['type'], 'sep')
|
|
159
|
+
self.assertTrue(entry['is_sep'])
|
|
160
|
+
self.assertFalse(entry['is_menu'])
|
|
161
|
+
|
|
162
|
+
def test_make_raw_menus(self):
|
|
163
|
+
# minimal test to ensure it calls the other method
|
|
164
|
+
with patch.object(self.handler, 'make_menus') as make_menus:
|
|
165
|
+
self.handler._make_raw_menus(self.request, foo='bar')
|
|
166
|
+
make_menus.assert_called_once_with(self.request, foo='bar')
|
|
167
|
+
|
|
168
|
+
def test_do_make_menus_prune_unallowed_item(self):
|
|
169
|
+
test_menus = [
|
|
170
|
+
{
|
|
171
|
+
'title': "First Menu",
|
|
172
|
+
'type': 'menu',
|
|
173
|
+
'items': [
|
|
174
|
+
{'title': "Foo", 'url': '#'},
|
|
175
|
+
{'title': "Bar", 'url': '#'},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
def is_allowed(request, item):
|
|
181
|
+
if item.get('title') == 'Bar':
|
|
182
|
+
return False
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
with patch.object(self.handler, 'make_menus', return_value=test_menus):
|
|
186
|
+
with patch.object(self.handler, '_is_allowed', side_effect=is_allowed):
|
|
187
|
+
menus = self.handler.do_make_menus(self.request)
|
|
188
|
+
|
|
189
|
+
# Foo remains but Bar is pruned
|
|
190
|
+
menu = menus[0]
|
|
191
|
+
self.assertEqual(len(menu['items']), 1)
|
|
192
|
+
item = menu['items'][0]
|
|
193
|
+
self.assertEqual(item['title'], 'Foo')
|
|
194
|
+
|
|
195
|
+
def test_do_make_menus_prune_unallowed_menu(self):
|
|
196
|
+
test_menus = [
|
|
197
|
+
{
|
|
198
|
+
'title': "First Menu",
|
|
199
|
+
'type': 'menu',
|
|
200
|
+
'items': [
|
|
201
|
+
{'title': "Foo", 'url': '#'},
|
|
202
|
+
{'title': "Bar", 'url': '#'},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
'title': "Second Menu",
|
|
207
|
+
'type': 'menu',
|
|
208
|
+
'items': [
|
|
209
|
+
{'title': "Baz", 'url': '#'},
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
def is_allowed(request, item):
|
|
215
|
+
if item.get('title') == 'Baz':
|
|
216
|
+
return True
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
with patch.object(self.handler, 'make_menus', return_value=test_menus):
|
|
220
|
+
with patch.object(self.handler, '_is_allowed', side_effect=is_allowed):
|
|
221
|
+
menus = self.handler.do_make_menus(self.request)
|
|
222
|
+
|
|
223
|
+
# Second/Baz remains but First/Foo/Bar are pruned
|
|
224
|
+
self.assertEqual(len(menus), 1)
|
|
225
|
+
menu = menus[0]
|
|
226
|
+
self.assertEqual(menu['title'], 'Second Menu')
|
|
227
|
+
self.assertEqual(len(menu['items']), 1)
|
|
228
|
+
item = menu['items'][0]
|
|
229
|
+
self.assertEqual(item['title'], 'Baz')
|
|
230
|
+
|
|
231
|
+
def test_do_make_menus_with_top_link(self):
|
|
232
|
+
test_menus = [
|
|
233
|
+
{
|
|
234
|
+
'title': "First Menu",
|
|
235
|
+
'type': 'menu',
|
|
236
|
+
'items': [
|
|
237
|
+
{'title': "Foo", 'url': '#'},
|
|
238
|
+
{'title': "Bar", 'url': '#'},
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
'title': "Second Link",
|
|
243
|
+
'type': 'link',
|
|
244
|
+
},
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
with patch.object(self.handler, 'make_menus', return_value=test_menus):
|
|
248
|
+
with patch.object(self.handler, '_is_allowed', return_value=True):
|
|
249
|
+
menus = self.handler.do_make_menus(self.request)
|
|
250
|
+
|
|
251
|
+
# ensure top link remains
|
|
252
|
+
self.assertEqual(len(menus), 2)
|
|
253
|
+
menu = menus[1]
|
|
254
|
+
self.assertEqual(menu['title'], "Second Link")
|
|
255
|
+
|
|
256
|
+
def test_do_make_menus_with_trailing_sep(self):
|
|
257
|
+
test_menus = [
|
|
258
|
+
{
|
|
259
|
+
'title': "First Menu",
|
|
260
|
+
'type': 'menu',
|
|
261
|
+
'items': [
|
|
262
|
+
{'title': "Foo", 'url': '#'},
|
|
263
|
+
{'title': "Bar", 'url': '#'},
|
|
264
|
+
{'type': 'sep'},
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
with patch.object(self.handler, 'make_menus', return_value=test_menus):
|
|
270
|
+
with patch.object(self.handler, '_is_allowed', return_value=True):
|
|
271
|
+
menus = self.handler.do_make_menus(self.request)
|
|
272
|
+
|
|
273
|
+
# ensure trailing sep was pruned
|
|
274
|
+
menu = menus[0]
|
|
275
|
+
self.assertEqual(len(menu['items']), 2)
|
|
276
|
+
foo, bar = menu['items']
|
|
277
|
+
self.assertEqual(foo['title'], 'Foo')
|
|
278
|
+
self.assertEqual(bar['title'], 'Bar')
|
|
279
|
+
|
|
280
|
+
def test_do_make_menus_with_submenu(self):
|
|
281
|
+
test_menus = [
|
|
282
|
+
{
|
|
283
|
+
'title': "First Menu",
|
|
284
|
+
'type': 'menu',
|
|
285
|
+
'items': [
|
|
286
|
+
{
|
|
287
|
+
'title': "First Submenu",
|
|
288
|
+
'type': 'menu',
|
|
289
|
+
'items': [
|
|
290
|
+
{'title': "Foo", 'url': '#'},
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
'title': "Second Submenu",
|
|
295
|
+
'type': 'menu',
|
|
296
|
+
'items': [
|
|
297
|
+
{'title': "Bar", 'url': '#'},
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
def is_allowed(request, item):
|
|
305
|
+
if item.get('title') == 'Bar':
|
|
306
|
+
return False
|
|
307
|
+
return True
|
|
308
|
+
|
|
309
|
+
with patch.object(self.handler, 'make_menus', return_value=test_menus):
|
|
310
|
+
with patch.object(self.handler, '_is_allowed', side_effect=is_allowed):
|
|
311
|
+
menus = self.handler.do_make_menus(self.request)
|
|
312
|
+
|
|
313
|
+
# first submenu remains, second is pruned
|
|
314
|
+
menu = menus[0]
|
|
315
|
+
self.assertEqual(len(menu['items']), 1)
|
|
316
|
+
submenu = menu['items'][0]
|
|
317
|
+
self.assertEqual(submenu['type'], 'submenu')
|
|
318
|
+
self.assertEqual(submenu['title'], 'First Submenu')
|
|
319
|
+
self.assertEqual(len(submenu['items']), 1)
|
|
320
|
+
item = submenu['items'][0]
|
|
321
|
+
self.assertEqual(item['title'], 'Foo')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|