From c5770392e32aeeaed9bd8dcf907a11c7df352b6c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 28 Jun 2022 18:14:57 -0400 Subject: [PATCH 1/4] Refactor ObjectChildrenView --- netbox/netbox/views/generic/bulk_views.py | 26 ++++---------- netbox/netbox/views/generic/mixins.py | 22 ++++++++++++ netbox/netbox/views/generic/object_views.py | 39 +++++++++++++-------- 3 files changed, 53 insertions(+), 34 deletions(-) create mode 100644 netbox/netbox/views/generic/mixins.py diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 96efc0de7..7267e73ed 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -24,6 +24,7 @@ from utilities.htmx import is_htmx from utilities.permissions import get_permission_for_model from utilities.views import GetReturnURLMixin from .base import BaseMultiObjectView +from .mixins import TableMixin __all__ = ( 'BulkComponentCreateView', @@ -36,9 +37,9 @@ __all__ = ( ) -class ObjectListView(BaseMultiObjectView): +class ObjectListView(BaseMultiObjectView, TableMixin): """ - Display multiple objects, all of the same type, as a table. + Display multiple objects, all the same type, as a table. Attributes: filterset: A django-filter FilterSet that is applied to the queryset @@ -61,20 +62,6 @@ class ObjectListView(BaseMultiObjectView): def get_required_permission(self): return get_permission_for_model(self.queryset.model, 'view') - def get_table(self, request, bulk_actions=True): - """ - Return the django-tables2 Table instance to be used for rendering the objects list. - - Args: - request: The current request - bulk_actions: Show checkboxes for object selection - """ - table = self.table(self.queryset, user=request.user) - if 'pk' in table.base_columns and bulk_actions: - table.columns.show('pk') - - return table - # # Export methods # @@ -159,7 +146,7 @@ class ObjectListView(BaseMultiObjectView): # Export the current table view if request.GET['export'] == 'table': - table = self.get_table(request, has_bulk_actions) + table = self.get_table(self.queryset, request, has_bulk_actions) columns = [name for name, _ in table.selected_columns] return self.export_table(table, columns) @@ -177,12 +164,11 @@ class ObjectListView(BaseMultiObjectView): # Fall back to default table/YAML export else: - table = self.get_table(request, has_bulk_actions) + table = self.get_table(self.queryset, request, has_bulk_actions) return self.export_table(table) # Render the objects table - table = self.get_table(request, has_bulk_actions) - table.configure(request) + table = self.get_table(self.queryset, request, has_bulk_actions) # If this is an HTMX request, return only the rendered table HTML if is_htmx(request): diff --git a/netbox/netbox/views/generic/mixins.py b/netbox/netbox/views/generic/mixins.py new file mode 100644 index 000000000..0adf3a4c4 --- /dev/null +++ b/netbox/netbox/views/generic/mixins.py @@ -0,0 +1,22 @@ +__all__ = ( + 'TableMixin', +) + + +class TableMixin: + + def get_table(self, data, request, bulk_actions=True): + """ + Return the django-tables2 Table instance to be used for rendering the objects list. + + Args: + data: Queryset or iterable containing table data + request: The current request + bulk_actions: Render checkboxes for object selection + """ + table = self.table(data, user=request.user) + if 'pk' in table.base_columns and bulk_actions: + table.columns.show('pk') + table.configure(request) + + return table diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 88abfa48f..f9d8b6ac9 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -1,4 +1,5 @@ import logging +from collections import defaultdict from copy import deepcopy from django.contrib import messages @@ -20,6 +21,7 @@ from utilities.permissions import get_permission_for_model from utilities.utils import get_viewname, normalize_querydict, prepare_cloned_fields from utilities.views import GetReturnURLMixin from .base import BaseObjectView +from .mixins import TableMixin __all__ = ( 'ComponentCreateView', @@ -69,23 +71,31 @@ class ObjectView(BaseObjectView): }) -class ObjectChildrenView(ObjectView): +class ObjectChildrenView(ObjectView, TableMixin): """ Display a table of child objects associated with the parent object. Attributes: - table: Table class used to render child objects list + child_model: The model class which represents the child objects + table: The django-tables2 Table class used to render the child objects list + filterset: A django-filter FilterSet that is applied to the queryset """ child_model = None table = None filterset = None + actions = ('bulk_edit', 'bulk_delete') + action_perms = defaultdict(set, **{ + 'bulk_edit': {'change'}, + 'bulk_delete': {'delete'}, + }) def get_children(self, request, parent): """ Return a QuerySet of child objects. - request: The current request - parent: The parent object + Args: + request: The current request + parent: The parent object """ raise NotImplementedError(f'{self.__class__.__name__} must implement get_children()') @@ -114,16 +124,16 @@ class ObjectChildrenView(ObjectView): if self.filterset: child_objects = self.filterset(request.GET, child_objects).qs - permissions = {} - for action in ('change', 'delete'): - perm_name = get_permission_for_model(self.child_model, action) - permissions[action] = request.user.has_perm(perm_name) + # Determine the available actions + actions = [] + for action in self.actions: + if request.user.has_perms([ + get_permission_for_model(self.child_model, name) for name in self.action_perms[action] + ]): + actions.append(action) - table = self.table(self.prep_table_data(request, child_objects, instance), user=request.user) - # Determine whether to display bulk action checkboxes - if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']): - table.columns.show('pk') - table.configure(request) + table_data = self.prep_table_data(request, child_objects, instance) + table = self.get_table(table_data, request, bool(actions)) # If this is an HTMX request, return only the rendered table HTML if is_htmx(request): @@ -134,8 +144,9 @@ class ObjectChildrenView(ObjectView): return render(request, self.get_template_name(), { 'object': instance, + 'child_model': self.child_model, 'table': table, - 'permissions': permissions, + 'actions': actions, **self.get_extra_context(request, instance), }) From cdcb77dea8b143c93369d5a34ded0fe128c01ea1 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 29 Jun 2022 14:06:01 -0400 Subject: [PATCH 2/4] Move actions determination to a mixin --- netbox/netbox/views/generic/bulk_views.py | 24 +++++--------------- netbox/netbox/views/generic/mixins.py | 25 +++++++++++++++++++++ netbox/netbox/views/generic/object_views.py | 20 +++++------------ 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 7267e73ed..bb1c2b8e3 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -24,7 +24,7 @@ from utilities.htmx import is_htmx from utilities.permissions import get_permission_for_model from utilities.views import GetReturnURLMixin from .base import BaseMultiObjectView -from .mixins import TableMixin +from .mixins import ActionsMixin, TableMixin __all__ = ( 'BulkComponentCreateView', @@ -37,7 +37,7 @@ __all__ = ( ) -class ObjectListView(BaseMultiObjectView, TableMixin): +class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): """ Display multiple objects, all the same type, as a table. @@ -51,13 +51,6 @@ class ObjectListView(BaseMultiObjectView, TableMixin): template_name = 'generic/object_list.html' filterset = None filterset_form = None - actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete') - action_perms = defaultdict(set, **{ - 'add': {'add'}, - 'import': {'add'}, - 'bulk_edit': {'change'}, - 'bulk_delete': {'delete'}, - }) def get_required_permission(self): return get_permission_for_model(self.queryset.model, 'view') @@ -134,12 +127,7 @@ class ObjectListView(BaseMultiObjectView, TableMixin): self.queryset = self.filterset(request.GET, self.queryset).qs # Determine the available actions - actions = [] - for action in self.actions: - if request.user.has_perms([ - get_permission_for_model(model, name) for name in self.action_perms[action] - ]): - actions.append(action) + actions = self.get_permitted_actions(request.user) has_bulk_actions = any([a.startswith('bulk_') for a in actions]) if 'export' in request.GET: @@ -176,15 +164,13 @@ class ObjectListView(BaseMultiObjectView, TableMixin): 'table': table, }) - context = { + return render(request, self.template_name, { 'model': model, 'table': table, 'actions': actions, 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None, **self.get_extra_context(request), - } - - return render(request, self.template_name, context) + }) class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView): diff --git a/netbox/netbox/views/generic/mixins.py b/netbox/netbox/views/generic/mixins.py index 0adf3a4c4..4b3fa0740 100644 --- a/netbox/netbox/views/generic/mixins.py +++ b/netbox/netbox/views/generic/mixins.py @@ -1,8 +1,33 @@ +from collections import defaultdict + +from utilities.permissions import get_permission_for_model + __all__ = ( 'TableMixin', ) +class ActionsMixin: + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete') + action_perms = defaultdict(set, **{ + 'add': {'add'}, + 'import': {'add'}, + 'bulk_edit': {'change'}, + 'bulk_delete': {'delete'}, + }) + + def get_permitted_actions(self, user, model=None): + """ + Return a tuple of actions for which the given user is permitted to do. + """ + model = model or self.queryset.model + return [ + action for action in self.actions if user.has_perms([ + get_permission_for_model(model, name) for name in self.action_perms[action] + ]) + ] + + class TableMixin: def get_table(self, data, request, bulk_actions=True): diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index f9d8b6ac9..82867b429 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -1,5 +1,4 @@ import logging -from collections import defaultdict from copy import deepcopy from django.contrib import messages @@ -21,7 +20,7 @@ from utilities.permissions import get_permission_for_model from utilities.utils import get_viewname, normalize_querydict, prepare_cloned_fields from utilities.views import GetReturnURLMixin from .base import BaseObjectView -from .mixins import TableMixin +from .mixins import ActionsMixin, TableMixin __all__ = ( 'ComponentCreateView', @@ -71,7 +70,7 @@ class ObjectView(BaseObjectView): }) -class ObjectChildrenView(ObjectView, TableMixin): +class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin): """ Display a table of child objects associated with the parent object. @@ -79,15 +78,13 @@ class ObjectChildrenView(ObjectView, TableMixin): child_model: The model class which represents the child objects table: The django-tables2 Table class used to render the child objects list filterset: A django-filter FilterSet that is applied to the queryset + actions: Supported actions for the model. When adding custom actions, bulk action names must + be prefixed with `bulk_`. Default actions: add, import, export, bulk_edit, bulk_delete + action_perms: A dictionary mapping supported actions to a set of permissions required for each """ child_model = None table = None filterset = None - actions = ('bulk_edit', 'bulk_delete') - action_perms = defaultdict(set, **{ - 'bulk_edit': {'change'}, - 'bulk_delete': {'delete'}, - }) def get_children(self, request, parent): """ @@ -125,12 +122,7 @@ class ObjectChildrenView(ObjectView, TableMixin): child_objects = self.filterset(request.GET, child_objects).qs # Determine the available actions - actions = [] - for action in self.actions: - if request.user.has_perms([ - get_permission_for_model(self.child_model, name) for name in self.action_perms[action] - ]): - actions.append(action) + actions = self.get_permitted_actions(request.user, model=self.child_model) table_data = self.prep_table_data(request, child_objects, instance) table = self.get_table(table_data, request, bool(actions)) From 4649bc632cddb530ca91dbab763de96602a3829c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 29 Jun 2022 14:21:57 -0400 Subject: [PATCH 3/4] Update templates for subclasses of ObjectChildrenView --- netbox/templates/dcim/device/consoleports.html | 4 ++-- netbox/templates/dcim/device/consoleserverports.html | 4 ++-- netbox/templates/dcim/device/devicebays.html | 4 ++-- netbox/templates/dcim/device/frontports.html | 4 ++-- netbox/templates/dcim/device/interfaces.html | 4 ++-- netbox/templates/dcim/device/inventory.html | 4 ++-- netbox/templates/dcim/device/modulebays.html | 4 ++-- netbox/templates/dcim/device/poweroutlets.html | 4 ++-- netbox/templates/dcim/device/powerports.html | 4 ++-- netbox/templates/dcim/device/rearports.html | 4 ++-- netbox/templates/ipam/aggregate/prefixes.html | 4 ++-- netbox/templates/ipam/iprange/ip_addresses.html | 4 ++-- netbox/templates/ipam/prefix/ip_addresses.html | 4 ++-- netbox/templates/ipam/prefix/ip_ranges.html | 4 ++-- netbox/templates/ipam/prefix/prefixes.html | 4 ++-- netbox/templates/virtualization/cluster/virtual_machines.html | 4 ++-- 16 files changed, 32 insertions(+), 32 deletions(-) diff --git a/netbox/templates/dcim/device/consoleports.html b/netbox/templates/dcim/device/consoleports.html index afc306bd4..04184be7c 100644 --- a/netbox/templates/dcim/device/consoleports.html +++ b/netbox/templates/dcim/device/consoleports.html @@ -17,7 +17,7 @@
- {% if perms.dcim.change_consoleport %} + {% if 'bulk_edit' in actions %} @@ -28,7 +28,7 @@ Disconnect {% endif %} - {% if perms.dcim.delete_consoleport %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/dcim/device/consoleserverports.html b/netbox/templates/dcim/device/consoleserverports.html index 5f244cdc7..ee1be91d7 100644 --- a/netbox/templates/dcim/device/consoleserverports.html +++ b/netbox/templates/dcim/device/consoleserverports.html @@ -17,7 +17,7 @@
- {% if perms.dcim.change_consoleserverport %} + {% if 'bulk_edit' in actions %} @@ -28,7 +28,7 @@ Disconnect {% endif %} - {% if perms.dcim.delete_consoleserverport %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/dcim/device/devicebays.html b/netbox/templates/dcim/device/devicebays.html index 5e33bdae0..7836935d9 100644 --- a/netbox/templates/dcim/device/devicebays.html +++ b/netbox/templates/dcim/device/devicebays.html @@ -17,7 +17,7 @@
- {% if perms.dcim.change_devicebay %} + {% if 'bulk_edit' in actions %} @@ -25,7 +25,7 @@ Edit {% endif %} - {% if perms.dcim.delete_devicebay %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/dcim/device/frontports.html b/netbox/templates/dcim/device/frontports.html index 0d0f9577c..8590fd50e 100644 --- a/netbox/templates/dcim/device/frontports.html +++ b/netbox/templates/dcim/device/frontports.html @@ -17,7 +17,7 @@
- {% if perms.dcim.change_frontport %} + {% if 'bulk_edit' in actions %} @@ -28,7 +28,7 @@ Disconnect {% endif %} - {% if perms.dcim.delete_frontport %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html index 22f6d8be5..7db7ea0ae 100644 --- a/netbox/templates/dcim/device/interfaces.html +++ b/netbox/templates/dcim/device/interfaces.html @@ -53,7 +53,7 @@
- {% if perms.dcim.change_interface %} + {% if 'bulk_edit' in actions %} @@ -64,7 +64,7 @@ Disconnect {% endif %} - {% if perms.dcim.delete_interface %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/dcim/device/inventory.html b/netbox/templates/dcim/device/inventory.html index 18a0712f3..de981c545 100644 --- a/netbox/templates/dcim/device/inventory.html +++ b/netbox/templates/dcim/device/inventory.html @@ -17,7 +17,7 @@
- {% if perms.dcim.change_inventoryitem %} + {% if 'bulk_edit' in actions %} @@ -25,7 +25,7 @@ Edit {% endif %} - {% if perms.dcim.delete_inventoryitem %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/dcim/device/modulebays.html b/netbox/templates/dcim/device/modulebays.html index fc1c9a60d..3e4dadb30 100644 --- a/netbox/templates/dcim/device/modulebays.html +++ b/netbox/templates/dcim/device/modulebays.html @@ -17,7 +17,7 @@
- {% if perms.dcim.change_modulebay %} + {% if 'bulk_edit' in actions %} @@ -25,7 +25,7 @@ Edit {% endif %} - {% if perms.dcim.delete_modulebay %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/dcim/device/poweroutlets.html b/netbox/templates/dcim/device/poweroutlets.html index d312fbbd0..f9880a4b1 100644 --- a/netbox/templates/dcim/device/poweroutlets.html +++ b/netbox/templates/dcim/device/poweroutlets.html @@ -17,7 +17,7 @@
- {% if perms.dcim.change_powerport %} + {% if 'bulk_edit' in actions %} @@ -28,7 +28,7 @@ Disconnect {% endif %} - {% if perms.dcim.delete_poweroutlet %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/dcim/device/powerports.html b/netbox/templates/dcim/device/powerports.html index cf71e81ba..fc426a023 100644 --- a/netbox/templates/dcim/device/powerports.html +++ b/netbox/templates/dcim/device/powerports.html @@ -17,7 +17,7 @@
- {% if perms.dcim.change_powerport %} + {% if 'bulk_edit' in actions %} @@ -28,7 +28,7 @@ Disconnect {% endif %} - {% if perms.dcim.delete_powerport %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/dcim/device/rearports.html b/netbox/templates/dcim/device/rearports.html index 73341990f..eee67b6fd 100644 --- a/netbox/templates/dcim/device/rearports.html +++ b/netbox/templates/dcim/device/rearports.html @@ -17,7 +17,7 @@
- {% if perms.dcim.change_rearport %} + {% if 'bulk_edit' in actions %} @@ -28,7 +28,7 @@ Disconnect {% endif %} - {% if perms.dcim.delete_rearport %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/ipam/aggregate/prefixes.html b/netbox/templates/ipam/aggregate/prefixes.html index d1b48429a..8256236f4 100644 --- a/netbox/templates/ipam/aggregate/prefixes.html +++ b/netbox/templates/ipam/aggregate/prefixes.html @@ -25,12 +25,12 @@
- {% if perms.ipam.change_prefix %} + {% if 'bulk_edit' in actions %} {% endif %} - {% if perms.ipam.delete_prefix %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/ipam/iprange/ip_addresses.html b/netbox/templates/ipam/iprange/ip_addresses.html index d9ac77fd0..61b2ee335 100644 --- a/netbox/templates/ipam/iprange/ip_addresses.html +++ b/netbox/templates/ipam/iprange/ip_addresses.html @@ -23,12 +23,12 @@
- {% if perms.ipam.change_ipaddress %} + {% if 'bulk_edit' in actions %} {% endif %} - {% if perms.ipam.delete_ipaddress %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/ipam/prefix/ip_addresses.html b/netbox/templates/ipam/prefix/ip_addresses.html index d734b825f..31a22497d 100644 --- a/netbox/templates/ipam/prefix/ip_addresses.html +++ b/netbox/templates/ipam/prefix/ip_addresses.html @@ -23,12 +23,12 @@
- {% if perms.ipam.change_ipaddress %} + {% if 'bulk_edit' in actions %} {% endif %} - {% if perms.ipam.delete_ipaddress %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/ipam/prefix/ip_ranges.html b/netbox/templates/ipam/prefix/ip_ranges.html index 268c290a1..45b1d4fd0 100644 --- a/netbox/templates/ipam/prefix/ip_ranges.html +++ b/netbox/templates/ipam/prefix/ip_ranges.html @@ -23,12 +23,12 @@
- {% if perms.ipam.change_iprange %} + {% if 'bulk_edit' in actions %} {% endif %} - {% if perms.ipam.delete_iprange %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/ipam/prefix/prefixes.html b/netbox/templates/ipam/prefix/prefixes.html index 5d42596ba..46fa29581 100644 --- a/netbox/templates/ipam/prefix/prefixes.html +++ b/netbox/templates/ipam/prefix/prefixes.html @@ -25,12 +25,12 @@
- {% if perms.ipam.change_prefix %} + {% if 'bulk_edit' in actions %} {% endif %} - {% if perms.ipam.delete_prefix %} + {% if 'bulk_delete' in actions %} diff --git a/netbox/templates/virtualization/cluster/virtual_machines.html b/netbox/templates/virtualization/cluster/virtual_machines.html index 953d9f940..9cb33258f 100644 --- a/netbox/templates/virtualization/cluster/virtual_machines.html +++ b/netbox/templates/virtualization/cluster/virtual_machines.html @@ -14,12 +14,12 @@
- {% if perms.virtualization.change_virtualmachine %} + {% if 'bulk_edit' in actions %} {% endif %} - {% if perms.virtualization.delete_virtualmachine %} + {% if 'bulk_delete' in actions %} From a0f9b5e47b29b5e04ba921741f4d24e588caf36f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 29 Jun 2022 14:30:47 -0400 Subject: [PATCH 4/4] Document support for ObjectChildrenView --- docs/plugins/development/views.md | 25 ++++++++++++++++--------- docs/release-notes/version-3.3.md | 1 + 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/plugins/development/views.md b/docs/plugins/development/views.md index 92626f8d3..cabcd7045 100644 --- a/docs/plugins/development/views.md +++ b/docs/plugins/development/views.md @@ -51,15 +51,16 @@ This makes our view accessible at the URL `/plugins/animal-sounds/random/`. (Rem NetBox provides several generic view classes (documented below) to facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use. -| View Class | Description | -|--------------------|--------------------------------| -| `ObjectView` | View a single object | -| `ObjectEditView` | Create or edit a single object | -| `ObjectDeleteView` | Delete a single object | -| `ObjectListView` | View a list of objects | -| `BulkImportView` | Import a set of new objects | -| `BulkEditView` | Edit multiple objects | -| `BulkDeleteView` | Delete multiple objects | +| View Class | Description | +|----------------------|--------------------------------------------------------| +| `ObjectView` | View a single object | +| `ObjectEditView` | Create or edit a single object | +| `ObjectDeleteView` | Delete a single object | +| `ObjectChildrenView` | A list of child objects within the context of a parent | +| `ObjectListView` | View a list of objects | +| `BulkImportView` | Import a set of new objects | +| `BulkEditView` | Edit multiple objects | +| `BulkDeleteView` | Delete multiple objects | !!! warning Please note that only the classes which appear in this documentation are currently supported. Although other classes may be present within the `views.generic` module, they are not yet supported for use by plugins. @@ -99,6 +100,12 @@ Below are the class definitions for NetBox's object views. These views handle CR members: - get_object +::: netbox.views.generic.ObjectChildrenView + selection: + members: + - get_children + - prep_table_data + ## Multi-Object Views Below are the class definitions for NetBox's multi-object views. These views handle simultaneous actions for sets objects. The list, import, edit, and delete views each inherit from `BaseMultiObjectView`, which is not intended to be used directly. diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 1e18de1e6..efcf570fa 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -34,6 +34,7 @@ ### Plugins API +* [#9092](https://github.com/netbox-community/netbox/issues/9092) - Add support for `ObjectChildrenView` generic view * [#9414](https://github.com/netbox-community/netbox/issues/9414) - Add `clone()` method to NetBoxModel for copying instance attributes ### Other Changes