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 diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 96efc0de7..bb1c2b8e3 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 ActionsMixin, TableMixin __all__ = ( 'BulkComponentCreateView', @@ -36,9 +37,9 @@ __all__ = ( ) -class ObjectListView(BaseMultiObjectView): +class ObjectListView(BaseMultiObjectView, ActionsMixin, 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 @@ -50,31 +51,10 @@ class ObjectListView(BaseMultiObjectView): 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') - 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 # @@ -147,19 +127,14 @@ class ObjectListView(BaseMultiObjectView): 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: # 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 +152,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): @@ -190,15 +164,13 @@ class ObjectListView(BaseMultiObjectView): '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 new file mode 100644 index 000000000..4b3fa0740 --- /dev/null +++ b/netbox/netbox/views/generic/mixins.py @@ -0,0 +1,47 @@ +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): + """ + 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..82867b429 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -20,6 +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 ActionsMixin, TableMixin __all__ = ( 'ComponentCreateView', @@ -69,12 +70,17 @@ class ObjectView(BaseObjectView): }) -class ObjectChildrenView(ObjectView): +class ObjectChildrenView(ObjectView, ActionsMixin, 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 + 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 @@ -84,8 +90,9 @@ class ObjectChildrenView(ObjectView): """ 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 +121,11 @@ 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 = self.get_permitted_actions(request.user, model=self.child_model) - 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 +136,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), }) 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 @@