Source code for wildewidgets.menus
from __future__ import annotations
import random
from typing import Any, ClassVar
from django import template
from django.urls import reverse
from wildewidgets.views import WidgetInitKwargsMixin
[docs]class BasicMenu(WidgetInitKwargsMixin):
"""
Basic menu widget.
A basic menu requires only one class attribute defined, :py:attr:`items`.
Example:
.. code-block:: python
from wildewidgets import BasicMenu
class MainMenu(BasicMenu):
items = [
('Users', 'core:home'),
('Uploads','core:uploads'),
]
"""
#: The Django template file to use for rendering the menu.
template_file: str = "wildewidgets/menu.html"
#: The CSS classes to use for the navbar. This can be set to
navbar_classes: str = "navbar-expand-lg navbar-light"
#: The CSS class to use for the container that holds the navbar.
container: str = "container-lg"
#: The image to use for the brand logo. If this is set, the brand text will
#: not be displayed. If you want to use a brand text, set this to
#: ``None``. The image should be a URL or a static file path.
brand_image: str | None = None
#: The CSS width to use for the brand image. This is only used if
#: :py:attr:`brand_image` is set. If you want the image to be responsive,
#: set this to ``100%``.
brand_image_width: str = "100%"
#: The text to use for the brand.
brand_text: str | None = None
#: The URL to use for the brand link. If this is set, the brand image or
#: text will be used as the link.
brand_url: str = "#"
#: A list of tuples that define the items to list in the menu, where the
#: first element is the menu item text and the second element is the URL
#: name. A third optional element in the tuple can be a dictionary of get
#: arguments
items: ClassVar[
list[tuple[str, str | list[tuple[str, str | None]] | dict[str, str] | None]]
] = []
def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
self.menu: dict[str, Any] = {}
self.active: str | None = None
if args:
self.active_hierarchy: list[str] = args[0].split("/")
else:
self.active_hierarchy = []
[docs] def parse_submemu(
self, items: list[tuple[str, str | None]], submenu_active: str | None
) -> dict[str, Any]:
"""
Parse and create a submenu structure.
This method processes a list of submenu items and creates a structured
representation of the submenu. It handles regular items and dividers,
and marks the appropriate item as active based on the submenu_active parameter.
Args:
items: List of tuples defining submenu items (title, url, [extra])
Keyword Args:
submenu_active: The name of the submenu item to mark as active
Returns:
dict: A dictionary containing the structured submenu data
"""
data: dict[str, Any] = {"kind": "submenu"}
sub_menu_items: list[dict[str, Any]] = []
for item in items:
if not isinstance(item, tuple):
continue
if item[0] == "divider":
subdata: dict[str, Any] = {"divider": True}
else:
subdata = {
"title": item[0],
"url": reverse(item[1]) if item[1] else "#",
"extra": "",
"divider": False,
"active": item[0] == submenu_active,
}
if len(item) > 2: # noqa: PLR2004
subdata["extra"] = self.convert_extra(item[2])
sub_menu_items.append(subdata)
data["items"] = sub_menu_items
return data
[docs] def get_content(self, **kwargs: Any) -> str: # noqa: ARG002
"""
Render the menu as HTML content.
This method:
1. Builds the menu structure
2. Creates a context dictionary with all necessary data
3. Renders the template with the context
Keyword Args:
**kwargs: Additional arguments (unused)
Returns:
str: The rendered HTML content for the menu
"""
self.build_menu()
context = {
"menu": self.menu,
"active": self.active,
"navbar_classes": self.navbar_classes,
"navbar_container": self.container,
"brand_image": self.brand_image,
"brand_image_width": self.brand_image_width,
"brand_text": self.brand_text,
"brand_url": self.brand_url,
"vertical": "navbar-vertical" in self.navbar_classes,
"target": random.randrange(0, 10000), # noqa: S311
}
html_template = template.loader.get_template(self.template_file)
return html_template.render(context)
def __str__(self) -> str:
"""
Return the string representation of the menu.
This method allows the menu to be used directly in templates via the
{{ menu }} syntax.
Returns:
str: The rendered HTML content for the menu
"""
return self.get_content()
[docs]class DarkMenu(BasicMenu):
"""
A dark-themed navigation menu.
This class extends the BasicMenu with dark styling using Bootstrap's dark
navbar classes. It provides a dark background with light text, suitable for
applications with darker themes.
Attributes:
navbar_classes: Bootstrap classes for styling the navbar with dark theme
Example:
.. code-block:: python
from wildewidgets import DarkMenu
class MainDarkMenu(DarkMenu):
items = [
('Dashboard', 'dashboard'),
('Reports', 'reports'),
]
"""
navbar_classes = "navbar-expand-lg navbar-dark bg-secondary"
[docs]class VerticalDarkMenu(BasicMenu):
"""
A vertical dark-themed navigation menu.
This class extends the BasicMenu with dark styling and vertical orientation.
It's particularly useful for sidebar navigation in applications with darker
themes.
Attributes:
navbar_classes: Bootstrap classes for styling a vertical dark navbar
Example:
.. code-block:: python
from wildewidgets import VerticalDarkMenu
class SidebarMenu(VerticalDarkMenu):
items = [
('Profile', 'user_profile'),
('Settings', 'user_settings'),
('Logout', 'logout'),
]
"""
navbar_classes = "navbar-vertical navbar-expand-lg navbar-dark"
[docs]class LightMenu(BasicMenu):
"""
A light-themed navigation menu.
This class extends the BasicMenu with light styling, which is also the
default for BasicMenu. It provides a light background with dark text,
suitable for standard application layouts.
Attributes:
navbar_classes: Bootstrap classes for styling the navbar with light theme
Example:
.. code-block:: python
from wildewidgets import LightMenu
class MainLightMenu(LightMenu):
items = [
('Home', 'home'),
('About', 'about'),
('Contact', 'contact'),
]
"""
navbar_classes = "navbar-expand-lg navbar-light"
[docs]class MenuMixin:
"""
A mixin for adding menu support to Django class-based views.
This mixin provides methods for including primary and secondary navigation menus
in Django views. It automatically adds menu instances to the template context,
making it easy to integrate navigation in templates.
To use this mixin:
1. Define menu classes by subclassing :py:class:`BasicMenu` or its variants
2. Add this mixin to your view classes
3. Set class attributes to specify which menus to use and which items to activate
You must define the :py:attr:`menu_class` and the :py:attr:`menu_item`, but
:py:attr:`submenu_class` and the :py:attr:`submenu_item` are optional. If
you do not set them, no secondary menu will be displayed.
Example:
With no secondary menu:
.. code-block:: python
from django.views.generic import TemplateView
from wildewidgets import BasicMenu, MenuMixin
class MainMenu(BasicMenu):
items = [
('Home', 'home'),
('About', 'about'),
('Contact', 'contact'),
]
class DashboardView(MenuMixin, TemplateView):
template_name = "dashboard.html"
menu_class = MainMenu
menu_item = "Home"
With a secondary menu:
.. code-block:: python
from django.views.generic import TemplateView
from wildewidgets import BasicMenu, LightMenu, MenuMixin
class MainMenu(BasicMenu):
items = [
('Home', 'home'),
('About', 'about'),
('Contact', 'contact'),
]
class DashboardSubmenu(LightMenu):
items = [
('Overview', 'dashboard:overview'),
('Statistics', 'dashboard:statistics'),
('Settings', 'dashboard:settings'),
]
class DashboardView(MenuMixin, TemplateView):
template_name = "dashboard.html"
menu_class = MainMenu
menu_item = "Home"
submenu_class = DashboardSubmenu
submenu_item = "Overview"
"""
#: The primary menu class to use for the view.
#: This should be a subclass of BasicMenu or one of its variants.
#: If set to None, no primary menu will be displayed.
menu_class: type[BasicMenu] | None = None
#: The active item for the primary menu.
menu_item: str | None = None
#: The secondary menu class to use for the view.
submenu_class: type[BasicMenu] | None = None
#: The active item for the secondary menu.
submenu_item: str | None = None
[docs] def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
"""
Add menu instances to the template context.
This method adds the primary and secondary menu instances to the context
if they are available.
Args:
**kwargs: Additional context variables
Returns:
The updated template context with menu instances
"""
menu = self.get_menu()
submenu = self.get_submenu()
if menu:
kwargs["menu"] = menu
if submenu:
kwargs["submenu"] = submenu
return super().get_context_data(**kwargs) # type: ignore[misc]