#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# -------------------------------------------------------------------------------
# This file is part of Mentat system (https://mentat.cesnet.cz/).
#
# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/)
# Use of this source is governed by the MIT license, see LICENSE file.
# -------------------------------------------------------------------------------
"""
This module contains useful menu representation *Hawat* application.
"""
__author__ = "Jan Mach <jan.mach@cesnet.cz>"
__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>"
import re
import collections
import werkzeug.routing
import flask
import flask_login
import flask_principal
import hawat.acl
CRE_STRIP_QUESTION = re.compile(r'\?$')
ENTRY_SUBMENU = 'submenu'
ENTRY_SUBMENUDB = 'submenudb'
ENTRY_VIEW = 'view'
ENTRY_ENDPOINT = 'endpoint'
ENTRY_LINK = 'link'
ENTRY_TEST = 'test'
def _url_segments(path):
parts = path.split('/')[1:]
if parts and parts[-1] == '':
parts.pop()
return parts
def _is_active(this_url, request):
request_path = request.script_root + request.path
request_path_full = request.script_root + request.full_path
# For some reason in certain cases the '?' is appended to the end of request
# path event in case there are no additional parameters. Get rid of that.
request_path_full = CRE_STRIP_QUESTION.sub('', request_path_full)
if len(this_url) > 1:
segments_url = _url_segments(this_url)
segments_request = _url_segments(request_path)
matching_segments = segments_request == segments_url
else:
matching_segments = False
matching_completpath = request_path_full == this_url
return matching_segments or matching_completpath
def _filter_menu_entries(entries, **kwargs):
"""
*Helper function*. Filter given list of menu entries for current user. During
the filtering following operations will be performed:
* Remove all entries accessible only for authenticated users, when the current
user is not authenticated.
* Remove all entries for which the current user has not sufficient permissions.
* Remove all empty submenu entries.
:param collections.OrderedDict entries: List of menu entries.
:param dict kwargs: Optional menu entry parameters.
:return: Filtered list of menu entries.
:rtype: collections.OrderedDict
"""
result = collections.OrderedDict()
for entry_id, entry in entries.items():
# print("Processing menu entry '{}'.".format(entry_id))
# Filter out entries protected with authentication.
if entry.authentication:
if not flask_login.current_user.is_authenticated:
# print("Hiding menu entry '{}', accessible only to authenticated users.".format(entry_id))
continue
# Filter out entries protected with authorization.
if entry.authorization:
hideflag = False
for authspec in entry.authorization:
# When accessing static files, there is no identity in g so the can() function would raise an exception.
if not 'identity' in flask.g:
hideflag = True
break
# Authorization rules may be specified as instances of flask_principal.Permission.
if isinstance(authspec, flask_principal.Permission):
if not authspec.can():
# print("Hiding menu entry '{}', accessible only to '{}'.".format(entry_id, str(authspec)))
hideflag = True
# Authorization rules may be specified as indices to hawat.acl permission dictionary.
else:
if not hawat.acl.PERMISSIONS[authspec].can():
# print("Hiding menu entry '{}', accessible only to '{}'.".format(entry_id, str(authspec)))
hideflag = True
if hideflag:
continue
if entry.type == ENTRY_SUBMENU:
# Filter out empty submenus.
if not _filter_menu_entries(entry._entries, **kwargs): # pylint: disable=locally-disabled,protected-access
# print("Hiding menu entry '{}', empty submenu.".format(entry_id))
continue
if entry.type == ENTRY_VIEW:
# Check item action authorization callback, if exists.
if hasattr(entry.view, 'authorize_item_action'):
params = entry._pick_params(kwargs) # pylint: disable=locally-disabled,protected-access
if not entry.view.authorize_item_action(**params):
# print("Hiding menu entry '{}', inaccessible item action for item '{}'.".format(entry_id, str(item)))
continue
# Check item change validation callback, if exists.
if hasattr(entry.view, 'validate_item_change'):
params = entry._pick_params(kwargs) # pylint: disable=locally-disabled,protected-access
if not entry.view.validate_item_change(**params):
# print("Hiding menu entry '{}', invalid item change for item '{}'.".format(entry_id, str(item)))
continue
result[entry_id] = entry
return result
def _get_menu_entries(entries, **kwargs):
"""
*Helper function*. Return filtered and sorted menu entries for current user.
:param collections.OrderedDict entries: List of menu entries.
:param item: Optional item for which the menu should be parametrized.
:return: Filtered list of menu entries.
:rtype: collections.OrderedDict
"""
return sorted(
list(
_filter_menu_entries(entries, **kwargs).values()
),
key=lambda x: x.position
)
# -------------------------------------------------------------------------------
[docs]class ViewEntry(MenuEntry):
"""
Class representing menu entries pointing to application views.
"""
def __init__(self, ident, **kwargs):
super().__init__(ident, **kwargs)
self.type = ENTRY_VIEW
self.view = kwargs['view']
self._url = kwargs.get('url', None)
@property
def endpoint(self):
"""
Property containing routing endpoint for current entry.
:return: Routing endpoint for current menu entry.
:rtype: str
"""
return self.view.get_view_endpoint()
@property
def authentication(self):
"""
Property containing authentication information for current entry.
:return: Authentication information for current menu entry.
:rtype: str
"""
return self.view.authentication
@property
def authorization(self):
"""
Property containing authorization information for current entry.
:return: Authorization information for current menu entry.
:rtype: str
"""
return self.view.authorization
[docs] def get_title(self, **kwargs):
params = self._pick_params(kwargs)
if not self.hidetitle:
value = self._title or self.view.get_menu_title(**params) or self.view.get_view_title(**params)
if value:
try:
return value(**params)
except TypeError:
return value
return None
[docs] def get_icon(self, **kwargs):
params = self._pick_params(kwargs)
if not self.hideicon:
value = self._icon or self.view.get_view_icon()
if value:
try:
return value(**params)
except TypeError:
return value
return 'missing-icon'
[docs] def get_legend(self, **kwargs):
params = self._pick_params(kwargs)
if not self.hidelegend:
value = self._legend or self.view.get_menu_legend(**params)
if value:
try:
return value(**params)
except TypeError:
return value
return None
[docs] def get_url(self, **kwargs):
"""
Return URL for current menu entry.
:param dict kwargs: Optional menu entry parameters.
:return: URL for current menu entry.
:rtype: str
"""
try:
params = self._pick_params(kwargs)
value = self._url or self.view.get_view_url(**params)
if value:
try:
return value(**params)
except TypeError:
return value
return flask.url_for(self.endpoint)
except werkzeug.routing.BuildError:
print("ERROR: Unable to build URL for {}:{}".format(self.ident, str(self.view)))
raise
[docs] def get_entries(self, **kwargs):
return []
[docs] def add_entry(self, ident, subentry):
raise RuntimeError(
"Unable to append submenu to '{}' view menu entry.".format(
self.ident
)
)
[docs] def is_active(self, request, **kwargs):
# print("Checking if view menu entry '{}' is active.".format(self.ident))
params = self._pick_params(kwargs)
return _is_active(
self.get_url(**params),
request
)
[docs]class EndpointEntry(ViewEntry):
"""
Class representing menu entries pointing to application routing endpoints.
"""
def __init__(self, ident, endpoint, **kwargs):
kwargs['view'] = flask.current_app.get_endpoint_class(endpoint)
super().__init__(ident, **kwargs)
[docs]class LinkEntry(MenuEntry):
"""
Class representing menu entries pointing to application views.
"""
def __init__(self, ident, **kwargs):
super().__init__(ident, **kwargs)
self.type = ENTRY_LINK
self.authentication = kwargs.get('authentication', False)
self.authorization = kwargs.get('authorization', [])
self._url = kwargs.get('url')
[docs] def get_url(self, **kwargs):
"""
Return URL for current menu entry.
:param dict kwargs: Optional menu entry parameters.
:return: URL for current menu entry.
:rtype: str
"""
params = self._pick_params(kwargs)
try:
return self._url(**params)
except TypeError:
return self._url
[docs] def get_entries(self, **kwargs):
return []
[docs] def add_entry(self, ident, subentry):
raise RuntimeError(
"Unable to append submenu to '{}' link menu entry.".format(
self.ident
)
)
[docs] def is_active(self, request, **kwargs):
# print("Checking if link menu entry '{}' is active.".format(self.ident))
params = self._pick_params(kwargs)
return _is_active(
self.get_url(**params),
request
)
[docs]class TestEntry(MenuEntry):
"""
Class for menu entries for testing and demonstration purposes.
"""
def __init__(self, ident, **kwargs):
super().__init__(ident, **kwargs)
self.type = ENTRY_TEST
self.authentication = kwargs.get('authentication', False)
self.authorization = kwargs.get('authorization', [])
[docs] def get_entries(self, **kwargs):
return []
[docs] def add_entry(self, ident, subentry):
raise RuntimeError(
"Unable to append submenu to '{}' test menu entry.".format(
self.ident
)
)
[docs] def is_active(self, request, **kwargs):
raise RuntimeError(
"This method makes no sense for test menu entries."
)
# -------------------------------------------------------------------------------
if __name__ == '__main__':
MENU = Menu()
MENU.add_entry('test', 'test0', position=10)
MENU.add_entry('test', 'test1', position=10)
MENU.add_entry('test', 'test2', position=20)
MENU.add_entry('test', 'test3', position=40)
MENU.add_entry('test', 'test4', position=30)
MENU.add_entry('submenu', 'sub1', position=50)
MENU.add_entry('submenu', 'sub2', position=60)
MENU.add_entry('test', 'sub1.test1', position=10)
MENU.add_entry('test', 'sub1.test2', position=20)
MENU.add_entry('test', 'sub1.test3', position=40)
MENU.add_entry('test', 'sub1.test4', position=30)
MENU.add_entry('test', 'sub2.test1', position=10)
MENU.add_entry('test', 'sub2.test2', position=20)
MENU.add_entry('test', 'sub2.test3', position=40)
MENU.add_entry('test', 'sub2.test4', position=30)
import pprint
pprint.pprint(MENU.get_entries())
pprint.pprint(MENU.__class__)