Source code for superdjango.ui.options.menus
__author__ = "Shawn Davis <shawn@superdjango.com>"
__maintainer__ = "Shawn Davis <shawn@superdjango.com>"
__version__ = "0.4.0-d"
# Imports
from django.core.exceptions import ImproperlyConfigured
from django.urls import reverse, NoReverseMatch, path
import inspect
from superdjango.ui.runtime.menus import Item as RuntimeItem, Menu as RuntimeMenu
from superdjango.utils import camelcase_to_underscore, underscore_to_title_case
from ..constants import MENU
from .interfaces import ModelUI
# Exports
# Classes
[docs]class Menu(object):
"""A menu which contains one or more menu items.
This option class is meant to be extended and registered:
.. code-block:: python
from superdjango import ui
class MyMenu(ui.Menu):
items = [
# ...
]
ui.site.register(MyMenu)
"""
flat = False
"""Indicates menu items should be displayed in sequence rather than nested. Templates must provide support."""
icon = None
"""The menu icon to display with the menu. Templates must provide support."""
items = list()
"""A list of items to be included in the menu; a MenuItem, MenuSeparator, or ModelUI."""
kwargs = dict()
"""Additional keyword arguments to be passed to the runtime menu instance."""
label = None
"""The label of the menu. Templates must provide support."""
location = MENU.MAIN
"""The location of the menu."""
name = None
"""A unique name for the menu. If omitted, a name will be automatically assigned."""
prefix = None
"""The URL prefix of items in the menu."""
root = None
"""the item which serves as the root for all other menu items."""
sort_order = None
"""The order in which the menu appears in the given location. If omitted, an order is automatically applied, but you
may wish to set this explicitly for the best results.
"""
[docs] def __init__(self, site=None):
"""Initialize the menu.
:param site: The current site instance.
:type site: superdjango.ui.options.interfaces.SiteUI
"""
self.site = site
def __repr__(self):
return "<%s %s:%s>" % (self.__class__.__name__, self.location, self.label)
[docs] def as_runtime(self, request):
"""Export the menu as a runtime instance.
:param request: The current HTTP request instance.
:rtype: RuntimeMenu | RuntimeItem | None
"""
items = self.get_items(request)
if len(items) > 1:
# Resort the sub-items.
items.sort(key=lambda x: x.sort_order)
# Handle sub-menus.
# i = 0
# for item in items:
# if isinstance(item, Menu):
# submenu = item.as_runtime(request)
# items[i] = submenu
#
# i += 1
# Create a runtime menu based on the configured instance.
menu = RuntimeMenu(
self.label,
self.name,
flat=self.flat,
icon=self.icon,
sort_order=self.sort_order,
**self.kwargs
)
# Set the menu items and return the runtime instance.
_items = list()
for item in items:
if isinstance(item, Menu):
submenu = item.as_runtime(request)
if submenu is not None:
_items.append(submenu)
else:
_items.append(item.as_runtime(request))
menu.items = _items
return menu
elif len(items) == 1:
menu = items[0].as_runtime(request)
if not menu.icon:
menu.icon = self.icon
if not menu.sort_order:
menu.sort_order = self.sort_order
return menu
else:
# The user may not be logged in.
return None
# noinspection PyMethodMayBeStatic,PyUnusedLocal
[docs] def check_permission(self, request):
"""Determine whether the current user has access to the menu as a whole.
:param request: The current HTTP request instance.
:rtype: bool
.. note::
By default, ``True`` is always returned. You may extend this class and override the method to impelment
your own permission check.
"""
return True
[docs] def get_items(self, request):
"""Get the items in the menu.
:param request: The current HTTP request instance.
:rtype: list[Menu | MenuItem | MenuSeparator | MenuView]
"""
a = list()
sort_order = 0
for item in self.items:
# ModelUI instances are instantiated during SiteUI.register() or get_urls().
if isinstance(item, ModelUI):
# Makes it possible to acquire the URL of a UI in get_urls() but still skip inclusion of the UI in
# menus.
if not item.menu_enabled:
continue
# Make sure the user has permission to access the model UI's index.
if not item.check_permission(request, item.index_name):
continue
# Normalize the UI index to a MenuItem instance.
label = item.get_verbose_name_plural()
pattern_name = "%s_%s" % (item.meta.model_name, item.index_name)
url = item.get_index_url()
if url is not None:
if item.sort_order is None:
item.sort_order = sort_order
a.append(MenuItem(label, pattern_name=pattern_name, sort_order=item.sort_order, url=url))
elif isinstance(item, Menu):
if item.sort_order is None:
item.sort_order = sort_order
if item.check_permission(request):
a.append(item)
elif isinstance(item, MenuItem):
if item.sort_order is None:
item.sort_order = sort_order
if item.check_permission(request):
a.append(item)
elif isinstance(item, MenuSeparator):
if item.sort_order is None:
item.sort_order = sort_order
# BUG: Menu separators cause a blank menu to appear when a user is not logged in. Skipping separators
# for unauthenticated users is all we can do.
if request.user.is_authenticated:
a.append(item)
elif isinstance(item, MenuView):
if item.sort_order is None:
item.sort_order = sort_order
a.append(item)
else:
pass
sort_order += 1
return a
[docs] def get_urls(self):
"""Get the URLs of the items contained within the menu.
:rtype: list()
"""
# When menus are used apart from SiteUI, an item may be an un-instantiated ModelUI class. If so, we create the
# instance here and replace the class with the actual instance.
index = 0
for item in self.items:
if inspect.isclass(item):
_item = item(site=self.site)
self.items[index] = _item
index += 1
# If a ModelUI subclass is specified as menu root, the get_root_url() will break because the UI is not
# instantiated. Here we attempt to match the class name of menu root to an already instantiated item.
if self.root is not None and inspect.isclass(self.root):
for item in self.items:
if self.root.__name__ == item.__class__.__name__:
self.root = item
break
# Get the URLs from each item in the menu.
count = 0
urls = list()
for item in self.items:
if isinstance(item, ModelUI):
# If this is the first UI in the list of items, it is considered the menu "root" unless a root has
# already been defined. This facilitates the proper handling of breadcrumbs. See
# UIModelView.get_breacrumbs().
if self.root is None and count == 0:
self.root = item
count += 1
urls += item.get_urls(prefix=self.prefix)
elif isinstance(item, Menu):
if not item.name:
item.name = item.__class__.__name__.lower().replace("menu", "")
if not item.site:
item.site = self.site
urls += item.get_urls()
elif isinstance(item, MenuView):
urls.append(item.get_path(prefix=self.prefix))
else:
pass
return urls
@property
def is_separator(self):
"""Indicates this menu is not a separator.
:rtype: bool
"""
return False
[docs]class MenuItem(object):
"""An individual menu item.
This class is meant to be instantiated as part of a menus items:
.. code-block:: python
from superdjango import ui
class MyMenu(ui.Menu):
items = [
ui.MenuItem(_("Help"), icon="fas fa-life-ring", url="/help/"),
ui.MenuItem(_("Log In"), authenticated=None, url="/login/"),
ui.MenuItem(_("Log Out"), authenticated=True, icon="fas fa-sign-out-alt", url="/logout/"),
]
location = ui.MENU.SECONDARY
"""
[docs] def __init__(self, label, args=None, authenticated=True, icon=None, kwargs=None, pattern_name=None,
permissions=None, prefix=None, sort_order=None, url=None):
"""Define a menu item.
:param label: The label of the item.
:type label: str
:param args: Pattern arguments. See ``get_url()``.
:type args: list
:param authenticated: Indicates whether a user must be authenticated to see the item. Setting this to ``None``
indicates the menu is not available when the user *is* authenticated.
:type authenticated: bool | None
:param icon: The FontAwesome icon to use for the menu item.
:type icon: str
:param kwargs: Pattern keyword arguments. See ``get_url()``.
:param pattern_name: The pattern name to reverse for the item. See ``get_url()``.
:type pattern_name: str
:param permissions: The as list of permissions required to see the item in the form of
``app_label.permission_name``. Permissions are checked *after* authentication.
:type permissions: list[str]
:param prefix: The URL prefix.
:type prefix: str
:param url: A partial or absolute URL. If prefix is used, the URL should be provided *without* the prefix.
:type url: str
"""
self.args = args
self.authenticated = authenticated
self.icon = icon
self.kwargs = kwargs
self.label = label
self.pattern_name = pattern_name
self.permissions = permissions
self.prefix = prefix
self.sort_order = sort_order
self._url = url
def __repr__(self):
return "<%s %s. %s>" % (self.__class__.__name__, self.sort_order, self.label)
[docs] def as_runtime(self, request):
"""Export the item as a runtime menu item.
:param request: The current HTTP request instance.
:rtype: RuntimeItem
"""
active = False
if self.pattern_name:
# https://stackoverflow.com/a/27379100/241720
# sharedslide_list sharedslide signage_slides_sharedslide_detail
# sharedslide_list sharedslide signage_slides_sharedslide_list
object_name = self.pattern_name.split("_")[-2]
# print(self.pattern_name, self.pattern_name.split("_")[-2], request.resolver_match.url_name)
if object_name in request.resolver_match.url_name:
# print(object_name, "in", request.resolver_match.url_name, "active = True")
active = True
elif request.resolver_match.url_name.endswith(self.pattern_name):
# print(request.resolver_match.url_name, "ends with", self.pattern_name, "active = True")
active = True
else:
# print("no match for", self.pattern_name, "active = False")
pass
item = RuntimeItem(
self.label,
self.name,
icon=self.icon,
sort_order=self.sort_order,
url=self.url,
active=active
)
return item
[docs] def check_permission(self, request):
"""Check permissions.
:param request: The current HTTP request instance.
:rtype: bool
"""
if self.authenticated is True and not request.user.is_authenticated:
return False
if self.authenticated is None and request.user.is_authenticated:
return False
if self.permissions is not None:
for perm in self.permissions:
if not request.user.has_perm(perm):
return False
return True
[docs] def get_url(self):
"""Get the URL for the item.
:rtype: str
:raise: ImproperlyConfigured
"""
if self._url is not None:
if self.prefix is not None and not self._url.startswith("http"):
return "%s%s" % (self.prefix, self._url)
return self._url
if self.pattern_name is not None:
try:
return reverse(self.pattern_name, args=self.args, kwargs=self.kwargs)
except NoReverseMatch:
pass
raise ImproperlyConfigured("No URL for reverse for menu item: %s" % self.label)
@property
def is_separator(self):
"""Indicates this item is not a separator.
:rtype: bool
"""
return False
@property
def name(self):
"""Alias for ``pattern_name``.
:rtype: str
"""
return self.pattern_name or ""
@property
def url(self):
"""Alias for ``get_url()``."""
return self.get_url()
[docs]class MenuSeparator(object):
"""A separator for menu items.
This class is meant to be instantiated as part of a menus items:
.. code-block:: python
from superdjango import ui
class MyMenu(ui.Menu):
items = [
# ...
ui.MenuSeparator(),
# ...
]
"""
icon = None
label = None
sort_order = None
url = None
[docs] def __init__(self, sort_order=None):
"""Initialize the separator.
:param sort_order: The order in which the separator appears.
:type sort_order: int
"""
self.sort_order = sort_order
def __repr__(self):
return "<%s %s. %s>" % (self.__class__.__name__, self.sort_order, self.label or "Separator")
# noinspection PyUnusedLocal
[docs] def as_runtime(self, request):
"""Just returns ``self``. No additional work required."""
return self
@property
def is_separator(self):
"""Indicates this item is a separator.
:rtype: bool
"""
return True
[docs]class MenuView(object):
"""A view that is incorporated into the menu system."""
[docs] def __init__(self, view, icon=None, label=None, menu_enabled=True, sort_order=None):
"""Initialize the menu view.
:param view: The view class.
:param icon: The icon for the view.
:type icon: str
:param label: The label of the the menu item.
:type label: str
:param menu_enabled: Indicates the view should be included in the menu. Setting this to ``False`` can be useful
to initialize the view without actually including it in a menu.
:type menu_enabled: bool
:param sort_order: The order in which the view should appear in menus.
:type sort_order: int
"""
self.icon = icon
self.menu_enabled = menu_enabled
self.sort_order = sort_order
self.view = view
self._url = None
if label is not None:
self.label = label
else:
try:
self.label = view.title
except AttributeError:
self.label = underscore_to_title_case(camelcase_to_underscore(view.__name__))
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self.view.__name__)
[docs] def as_runtime(self, request):
"""Export the view as a runtime menu item."""
if not self.menu_enabled:
return None
active = False
if request.resolver_match.url_name == self.view.pattern_name:
active = True
name = self.view.pattern_name
return RuntimeItem(
label=self.label,
name=name,
icon=self.icon,
url=self.url,
active=active
)
[docs] def get_path(self, prefix=None):
"""Get the URL path."""
try:
pattern_name = self.view.pattern_name
except AttributeError:
pattern_name = None
try:
pattern_value = self.view.pattern_value
except AttributeError:
pattern_value = None
if any([pattern_name is None, pattern_value is None]):
message = "The %s view of a MenuView must specify both pattern_name and pattern_value."
raise ImproperlyConfigured(message % self.view.__name__)
if prefix is not None:
pattern_value = "%s/%s" % (prefix, pattern_value)
return path(pattern_value, self.view.as_view(), name=pattern_name)
def get_url(self):
self._url = reverse(self.view.pattern_name)
return self._url
@property
def is_separator(self):
"""Indicates this item is not a separator.
:rtype: bool
"""
return False
# @property
# def name(self):
# """Alias for ``view.pattern_name``.
#
# :rtype: str
#
# """
# return self.pattern_name or ""
@property
def url(self):
"""Alias for ``get_url()``."""
return self._url or self.get_url()