Browse Source

Docs and test for #9072

jeremystretch 3 năm trước cách đây
mục cha
commit
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
 ## 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:
 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
 * `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
 ### Plugins API
 
 
 * [#9071](https://github.com/netbox-community/netbox/issues/9071) - Introduce `PluginMenu` for top-level plugin navigation menus
 * [#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
 * [#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
 * [#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.http import HttpResponse
 from django.views.generic import View
 from django.views.generic import View
 
 
+from dcim.models import Site
+from utilities.views import register_model_view
 from .models import DummyModel
 from .models import DummyModel
 
 
 
 
@@ -9,3 +11,10 @@ class DummyModelsView(View):
     def get(self, request):
     def get(self, request):
         instance_count = DummyModel.objects.count()
         instance_count = DummyModel.objects.count()
         return HttpResponse(f"Instances: {instance_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)
         response = client.get(url)
         self.assertEqual(response.status_code, 200)
         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):
     def test_menu(self):
         """
         """
         Check menu registration.
         Check menu registration.

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

@@ -14,7 +14,6 @@ class BaseObjectView(ObjectPermissionRequiredMixin, View):
     """
     """
     queryset = None
     queryset = None
     template_name = None
     template_name = None
-    tab = None
 
 
     def get_object(self, **kwargs):
     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.
     Retrieve a single object for display.
 
 
     Note: If `template_name` is not specified, it will be determined automatically based on the queryset model.
     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):
     def get_required_permission(self):
         return get_permission_for_model(self.queryset.model, 'view')
         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
     ViewTabs are used for navigation among multiple object-specific views, such as the changelog or journal for
     a particular object.
     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):
     def __init__(self, label, badge=None, permission=None):
         self.label = label
         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
         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.
             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.
         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):
     def _wrapper(cls):
         app_label = model._meta.app_label
         app_label = model._meta.app_label