Răsfoiți Sursa

Docs and test for #9072

jeremystretch 3 ani în urmă
părinte
comite
053c97b7a8

+ 26 - 0
docs/plugins/development/views.md

@@ -148,6 +148,32 @@ These views are provided to enable or enhance certain NetBox model features, suc
 
 ## Extending Core Views
 
+### Additional Tabs
+
+Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`:
+
+```python
+from dcim.models import Site
+from myplugin.models import Stuff
+from netbox.views import generic
+from utilities.views import ViewTab, register_model_view
+
+@register_model_view(Site, 'mview', path='some-other-stuff')
+class MyView(generic.ObjectView):
+    ...
+    tab = ViewTab(
+        label='Other Stuff',
+        badge=lambda obj: Stuff.objects.filter(site=obj).count(),
+        permission='myplugin.view_stuff'
+    )
+```
+
+::: utilities.views.register_model_view
+
+::: utilities.views.ViewTab
+
+### Extra Template Content
+
 Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available:
 
 * `left_page()` - Inject content on the left side of the page

+ 1 - 0
docs/release-notes/version-3.4.md

@@ -26,6 +26,7 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
 ### Plugins API
 
 * [#9071](https://github.com/netbox-community/netbox/issues/9071) - Introduce `PluginMenu` for top-level plugin navigation menus
+* [#9072](https://github.com/netbox-community/netbox/issues/9072) - Enable registration of tabbed plugin views for core NetBox models
 * [#9880](https://github.com/netbox-community/netbox/issues/9880) - Introduce `django_apps` plugin configuration parameter
 * [#10314](https://github.com/netbox-community/netbox/issues/10314) - Move `clone()` method from NetBoxModel to CloningMixin
 

+ 9 - 0
netbox/extras/tests/dummy_plugin/views.py

@@ -1,6 +1,8 @@
 from django.http import HttpResponse
 from django.views.generic import View
 
+from dcim.models import Site
+from utilities.views import register_model_view
 from .models import DummyModel
 
 
@@ -9,3 +11,10 @@ class DummyModelsView(View):
     def get(self, request):
         instance_count = DummyModel.objects.count()
         return HttpResponse(f"Instances: {instance_count}")
+
+
+@register_model_view(Site, 'extra', path='other-stuff')
+class ExtraCoreModelView(View):
+
+    def get(self, request, pk):
+        return HttpResponse("Success!")

+ 11 - 0
netbox/extras/tests/test_plugins.py

@@ -59,6 +59,17 @@ class PluginTest(TestCase):
         response = client.get(url)
         self.assertEqual(response.status_code, 200)
 
+    def test_registered_views(self):
+
+        # Test URL resolution
+        url = reverse('dcim:site_extra', kwargs={'pk': 1})
+        self.assertEqual(url, '/dcim/sites/1/other-stuff/')
+
+        # Test GET request
+        client = Client()
+        response = client.get(url)
+        self.assertEqual(response.status_code, 200)
+
     def test_menu(self):
         """
         Check menu registration.

+ 0 - 1
netbox/netbox/views/generic/base.py

@@ -14,7 +14,6 @@ class BaseObjectView(ObjectPermissionRequiredMixin, View):
     """
     queryset = None
     template_name = None
-    tab = None
 
     def get_object(self, **kwargs):
         """

+ 5 - 0
netbox/netbox/views/generic/object_views.py

@@ -37,7 +37,12 @@ class ObjectView(BaseObjectView):
     Retrieve a single object for display.
 
     Note: If `template_name` is not specified, it will be determined automatically based on the queryset model.
+
+    Attributes:
+        tab: A ViewTab instance for the view
     """
+    tab = None
+
     def get_required_permission(self):
         return get_permission_for_model(self.queryset.model, 'view')
 

+ 7 - 1
netbox/utilities/views.py

@@ -137,6 +137,12 @@ class ViewTab:
     """
     ViewTabs are used for navigation among multiple object-specific views, such as the changelog or journal for
     a particular object.
+
+    Args:
+        label: Human-friendly text
+        badge: A static value or callable to display alongside the label (optional). If a callable is used, it must accept a single
+            argument representing the object being viewed.
+        permission: The permission required to display the tab (optional).
     """
     def __init__(self, label, badge=None, permission=None):
         self.label = label
@@ -178,7 +184,7 @@ def register_model_view(model, name, path=None, kwargs=None):
         name: The string used to form the view's name for URL resolution (e.g. via `reverse()`). This will be appended
             to the name of the base view for the model using an underscore.
         path: The URL path by which the view can be reached (optional). If not provided, `name` will be used.
-        kwargs: A dictionary of keyword arguments for the view to include when registering its URL path (optional)
+        kwargs: A dictionary of keyword arguments for the view to include when registering its URL path (optional).
     """
     def _wrapper(cls):
         app_label = model._meta.app_label