diff --git a/netbox/netbox/navigation/__init__.py b/netbox/netbox/navigation/__init__.py index 75ca8f440..9b61968bc 100644 --- a/netbox/netbox/navigation/__init__.py +++ b/netbox/netbox/navigation/__init__.py @@ -1,6 +1,8 @@ from dataclasses import dataclass from typing import Sequence, Optional +from django.urls import reverse_lazy + __all__ = ( 'get_model_item', @@ -22,20 +24,46 @@ class MenuItemButton: link: str title: str icon_class: str + _url: Optional[str] = None permissions: Optional[Sequence[str]] = () color: Optional[str] = None + def __post_init__(self): + if self.link: + self._url = reverse_lazy(self.link) + + @property + def url(self): + return self._url + + @url.setter + def url(self, value): + self._url = value + @dataclass class MenuItem: link: str link_text: str + _url: Optional[str] = None permissions: Optional[Sequence[str]] = () auth_required: Optional[bool] = False staff_only: Optional[bool] = False buttons: Optional[Sequence[MenuItemButton]] = () + def __post_init__(self): + if self.link: + self._url = reverse_lazy(self.link) + + @property + def url(self): + return self._url + + @url.setter + def url(self, value): + self._url = value + @dataclass class MenuGroup: diff --git a/netbox/netbox/plugins/navigation.py b/netbox/netbox/plugins/navigation.py index fc86b134a..2b18a4a0e 100644 --- a/netbox/netbox/plugins/navigation.py +++ b/netbox/netbox/plugins/navigation.py @@ -1,3 +1,4 @@ +from django.urls import reverse_lazy from django.utils.text import slugify from django.utils.translation import gettext as _ @@ -32,17 +33,23 @@ class PluginMenuItem: This class represents a navigation menu item. This constitutes primary link and its text, but also allows for specifying additional link buttons that appear to the right of the item in the van menu. - Links are specified as Django reverse URL strings. + Links are specified as Django reverse URL strings suitable for rendering via {% url item.link %}. + Alternatively, a pre-generated url can be set on the object which will be rendered literally. Buttons are each specified as a list of PluginMenuButton instances. """ permissions = [] buttons = [] + _url = None - def __init__(self, link, link_text, auth_required=False, staff_only=False, permissions=None, buttons=None): + def __init__( + self, link, link_text, auth_required=False, staff_only=False, permissions=None, buttons=None + ): self.link = link self.link_text = link_text self.auth_required = auth_required self.staff_only = staff_only + if link: + self._url = reverse_lazy(link) if permissions is not None: if type(permissions) not in (list, tuple): raise TypeError(_("Permissions must be passed as a tuple or list.")) @@ -52,6 +59,14 @@ class PluginMenuItem: raise TypeError(_("Buttons must be passed as a tuple or list.")) self.buttons = buttons + @property + def url(self): + return self._url + + @url.setter + def url(self, value): + self._url = value + class PluginMenuButton: """ @@ -60,11 +75,14 @@ class PluginMenuButton: """ color = ButtonColorChoices.DEFAULT permissions = [] + _url = None def __init__(self, link, title, icon_class, color=None, permissions=None): self.link = link self.title = title self.icon_class = icon_class + if link: + self._url = reverse_lazy(link) if permissions is not None: if type(permissions) not in (list, tuple): raise TypeError(_("Permissions must be passed as a tuple or list.")) @@ -73,3 +91,11 @@ class PluginMenuButton: if color not in ButtonColorChoices.values(): raise ValueError(_("Button color must be a choice within ButtonColorChoices.")) self.color = color + + @property + def url(self): + return self._url + + @url.setter + def url(self, value): + self._url = value diff --git a/netbox/utilities/templates/navigation/menu.html b/netbox/utilities/templates/navigation/menu.html index 3983915df..8becc568b 100644 --- a/netbox/utilities/templates/navigation/menu.html +++ b/netbox/utilities/templates/navigation/menu.html @@ -41,11 +41,11 @@ {% for item, buttons in items %}