From 053c97b7a8e0ef20fda3873108d981c92a129984 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 7 Oct 2022 15:03:52 -0400 Subject: [PATCH] Docs and test for #9072 --- docs/plugins/development/views.md | 26 +++++++++++++++++++++ docs/release-notes/version-3.4.md | 1 + netbox/extras/tests/dummy_plugin/views.py | 9 +++++++ netbox/extras/tests/test_plugins.py | 11 +++++++++ netbox/netbox/views/generic/base.py | 1 - netbox/netbox/views/generic/object_views.py | 5 ++++ netbox/utilities/views.py | 8 ++++++- 7 files changed, 59 insertions(+), 2 deletions(-) diff --git a/docs/plugins/development/views.md b/docs/plugins/development/views.md index cabcd7045..dfada7a42 100644 --- a/docs/plugins/development/views.md +++ b/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 diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 313a84f20..5e9fe0439 100644 --- a/docs/release-notes/version-3.4.md +++ b/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 diff --git a/netbox/extras/tests/dummy_plugin/views.py b/netbox/extras/tests/dummy_plugin/views.py index 4512758df..8713102c5 100644 --- a/netbox/extras/tests/dummy_plugin/views.py +++ b/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!") diff --git a/netbox/extras/tests/test_plugins.py b/netbox/extras/tests/test_plugins.py index e0ff67a2b..2eca3a3f7 100644 --- a/netbox/extras/tests/test_plugins.py +++ b/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. diff --git a/netbox/netbox/views/generic/base.py b/netbox/netbox/views/generic/base.py index 3a85df618..3ad3bcf67 100644 --- a/netbox/netbox/views/generic/base.py +++ b/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): """ diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 941eee72e..9aa71b01b 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/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') diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index f94f1842a..75c5aecff 100644 --- a/netbox/utilities/views.py +++ b/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