Răsfoiți Sursa

Closes: #19793 - Nav menu link customization (#19794)

* Support menu items that are callables

* Fix quote on add button

* Clarify docstring to differentiate link and url

* Back out support for callables but keep alternate prerendered url param

* Make url a property on MenuItem/PluginMenuItem etc, overridable via a setter

* Use reverse_lazy instead of reverse

* Use reverse_lazy instead of reverse
bctiemann 7 luni în urmă
părinte
comite
f5d32b1bf1

+ 28 - 0
netbox/netbox/navigation/__init__.py

@@ -1,6 +1,8 @@
 from dataclasses import dataclass
 from dataclasses import dataclass
 from typing import Sequence, Optional
 from typing import Sequence, Optional
 
 
+from django.urls import reverse_lazy
+
 
 
 __all__ = (
 __all__ = (
     'get_model_item',
     'get_model_item',
@@ -22,20 +24,46 @@ class MenuItemButton:
     link: str
     link: str
     title: str
     title: str
     icon_class: str
     icon_class: str
+    _url: Optional[str] = None
     permissions: Optional[Sequence[str]] = ()
     permissions: Optional[Sequence[str]] = ()
     color: Optional[str] = None
     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
 @dataclass
 class MenuItem:
 class MenuItem:
 
 
     link: str
     link: str
     link_text: str
     link_text: str
+    _url: Optional[str] = None
     permissions: Optional[Sequence[str]] = ()
     permissions: Optional[Sequence[str]] = ()
     auth_required: Optional[bool] = False
     auth_required: Optional[bool] = False
     staff_only: Optional[bool] = False
     staff_only: Optional[bool] = False
     buttons: Optional[Sequence[MenuItemButton]] = ()
     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
 @dataclass
 class MenuGroup:
 class MenuGroup:

+ 28 - 2
netbox/netbox/plugins/navigation.py

@@ -1,3 +1,4 @@
+from django.urls import reverse_lazy
 from django.utils.text import slugify
 from django.utils.text import slugify
 from django.utils.translation import gettext as _
 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
     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.
     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.
     Buttons are each specified as a list of PluginMenuButton instances.
     """
     """
     permissions = []
     permissions = []
     buttons = []
     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 = link
         self.link_text = link_text
         self.link_text = link_text
         self.auth_required = auth_required
         self.auth_required = auth_required
         self.staff_only = staff_only
         self.staff_only = staff_only
+        if link:
+            self._url = reverse_lazy(link)
         if permissions is not None:
         if permissions is not None:
             if type(permissions) not in (list, tuple):
             if type(permissions) not in (list, tuple):
                 raise TypeError(_("Permissions must be passed as a tuple or list."))
                 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."))
                 raise TypeError(_("Buttons must be passed as a tuple or list."))
             self.buttons = buttons
             self.buttons = buttons
 
 
+    @property
+    def url(self):
+        return self._url
+
+    @url.setter
+    def url(self, value):
+        self._url = value
+
 
 
 class PluginMenuButton:
 class PluginMenuButton:
     """
     """
@@ -60,11 +75,14 @@ class PluginMenuButton:
     """
     """
     color = ButtonColorChoices.DEFAULT
     color = ButtonColorChoices.DEFAULT
     permissions = []
     permissions = []
+    _url = None
 
 
     def __init__(self, link, title, icon_class, color=None, permissions=None):
     def __init__(self, link, title, icon_class, color=None, permissions=None):
         self.link = link
         self.link = link
         self.title = title
         self.title = title
         self.icon_class = icon_class
         self.icon_class = icon_class
+        if link:
+            self._url = reverse_lazy(link)
         if permissions is not None:
         if permissions is not None:
             if type(permissions) not in (list, tuple):
             if type(permissions) not in (list, tuple):
                 raise TypeError(_("Permissions must be passed as a tuple or list."))
                 raise TypeError(_("Permissions must be passed as a tuple or list."))
@@ -73,3 +91,11 @@ class PluginMenuButton:
             if color not in ButtonColorChoices.values():
             if color not in ButtonColorChoices.values():
                 raise ValueError(_("Button color must be a choice within ButtonColorChoices."))
                 raise ValueError(_("Button color must be a choice within ButtonColorChoices."))
             self.color = color
             self.color = color
+
+    @property
+    def url(self):
+        return self._url
+
+    @url.setter
+    def url(self, value):
+        self._url = value

+ 2 - 2
netbox/utilities/templates/navigation/menu.html

@@ -41,11 +41,11 @@
               </div>
               </div>
               {% for item, buttons in items %}
               {% for item, buttons in items %}
                 <div class="dropdown-item d-flex justify-content-between ps-3 py-0">
                 <div class="dropdown-item d-flex justify-content-between ps-3 py-0">
-                  <a href="{% url item.link %}" class="d-inline-flex flex-fill py-1">{{ item.link_text }}</a>
+                  <a href="{{ item.url }}" class="d-inline-flex flex-fill py-1">{{ item.link_text }}</a>
                   {% if buttons %}
                   {% if buttons %}
                     <div class="btn-group ms-1">
                     <div class="btn-group ms-1">
                       {% for button in buttons %}
                       {% for button in buttons %}
-                        <a href="{% url button.link %}" class="btn btn-sm btn-{{ button.color|default:"outline" }} lh-2 px-2" title="{{ button.title }}">
+                        <a href="{{ button.url }}" class="btn btn-sm btn-{{ button.color|default:"outline" }} lh-2 px-2" title="{{ button.title }}">
                           <i class="{{ button.icon_class }}"></i>
                           <i class="{{ button.icon_class }}"></i>
                         </a>
                         </a>
                       {% endfor %}
                       {% endfor %}