Docs and test for #9072

This commit is contained in:
jeremystretch 2022-10-07 15:03:52 -04:00
parent 1fc8de85a3
commit 053c97b7a8
7 changed files with 59 additions and 2 deletions

View File

@ -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

View File

@ -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

View File

@ -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!")

View File

@ -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.

View File

@ -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):
""" """

View File

@ -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')

View File

@ -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