Просмотр исходного кода

Merge pull request #9630 from netbox-community/9092-ObjectChildrenView

Closes #9092: Introduce support for use of ObjectChildrenView by plugins
Jeremy Stretch 3 лет назад
Родитель
Сommit
2245f1bf41

+ 16 - 9
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.
 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
 !!! 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.
     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:
       members:
         - get_object
         - get_object
 
 
+::: netbox.views.generic.ObjectChildrenView
+    selection:
+      members:
+        - get_children
+        - prep_table_data
+
 ## Multi-Object Views
 ## 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.
 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.

+ 1 - 0
docs/release-notes/version-3.3.md

@@ -34,6 +34,7 @@
 
 
 ### Plugins API
 ### 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
 * [#9414](https://github.com/netbox-community/netbox/issues/9414) - Add `clone()` method to NetBoxModel for copying instance attributes
 
 
 ### Other Changes
 ### Other Changes

+ 9 - 37
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.permissions import get_permission_for_model
 from utilities.views import GetReturnURLMixin
 from utilities.views import GetReturnURLMixin
 from .base import BaseMultiObjectView
 from .base import BaseMultiObjectView
+from .mixins import ActionsMixin, TableMixin
 
 
 __all__ = (
 __all__ = (
     'BulkComponentCreateView',
     '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:
     Attributes:
         filterset: A django-filter FilterSet that is applied to the queryset
         filterset: A django-filter FilterSet that is applied to the queryset
@@ -50,31 +51,10 @@ class ObjectListView(BaseMultiObjectView):
     template_name = 'generic/object_list.html'
     template_name = 'generic/object_list.html'
     filterset = None
     filterset = None
     filterset_form = 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):
     def get_required_permission(self):
         return get_permission_for_model(self.queryset.model, 'view')
         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
     # Export methods
     #
     #
@@ -147,19 +127,14 @@ class ObjectListView(BaseMultiObjectView):
             self.queryset = self.filterset(request.GET, self.queryset).qs
             self.queryset = self.filterset(request.GET, self.queryset).qs
 
 
         # Determine the available actions
         # 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])
         has_bulk_actions = any([a.startswith('bulk_') for a in actions])
 
 
         if 'export' in request.GET:
         if 'export' in request.GET:
 
 
             # Export the current table view
             # Export the current table view
             if request.GET['export'] == 'table':
             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]
                 columns = [name for name, _ in table.selected_columns]
                 return self.export_table(table, columns)
                 return self.export_table(table, columns)
 
 
@@ -177,12 +152,11 @@ class ObjectListView(BaseMultiObjectView):
 
 
             # Fall back to default table/YAML export
             # Fall back to default table/YAML export
             else:
             else:
-                table = self.get_table(request, has_bulk_actions)
+                table = self.get_table(self.queryset, request, has_bulk_actions)
                 return self.export_table(table)
                 return self.export_table(table)
 
 
         # Render the objects 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 this is an HTMX request, return only the rendered table HTML
         if is_htmx(request):
         if is_htmx(request):
@@ -190,15 +164,13 @@ class ObjectListView(BaseMultiObjectView):
                 'table': table,
                 'table': table,
             })
             })
 
 
-        context = {
+        return render(request, self.template_name, {
             'model': model,
             'model': model,
             'table': table,
             'table': table,
             'actions': actions,
             'actions': actions,
             'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
             'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
             **self.get_extra_context(request),
             **self.get_extra_context(request),
-        }
-
-        return render(request, self.template_name, context)
+        })
 
 
 
 
 class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView):
 class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView):

+ 47 - 0
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

+ 17 - 14
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.utils import get_viewname, normalize_querydict, prepare_cloned_fields
 from utilities.views import GetReturnURLMixin
 from utilities.views import GetReturnURLMixin
 from .base import BaseObjectView
 from .base import BaseObjectView
+from .mixins import ActionsMixin, TableMixin
 
 
 __all__ = (
 __all__ = (
     'ComponentCreateView',
     '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.
     Display a table of child objects associated with the parent object.
 
 
     Attributes:
     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
     child_model = None
     table = None
     table = None
@@ -84,8 +90,9 @@ class ObjectChildrenView(ObjectView):
         """
         """
         Return a QuerySet of child objects.
         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()')
         raise NotImplementedError(f'{self.__class__.__name__} must implement get_children()')
 
 
@@ -114,16 +121,11 @@ class ObjectChildrenView(ObjectView):
         if self.filterset:
         if self.filterset:
             child_objects = self.filterset(request.GET, child_objects).qs
             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 this is an HTMX request, return only the rendered table HTML
         if is_htmx(request):
         if is_htmx(request):
@@ -134,8 +136,9 @@ class ObjectChildrenView(ObjectView):
 
 
         return render(request, self.get_template_name(), {
         return render(request, self.get_template_name(), {
             'object': instance,
             'object': instance,
+            'child_model': self.child_model,
             'table': table,
             'table': table,
-            'permissions': permissions,
+            'actions': actions,
             **self.get_extra_context(request, instance),
             **self.get_extra_context(request, instance),
         })
         })
 
 

+ 2 - 2
netbox/templates/dcim/device/consoleports.html

@@ -17,7 +17,7 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
         <div class="bulk-button-group">
-            {% if perms.dcim.change_consoleport %}
+            {% if 'bulk_edit' in actions %}
                 <button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                 <button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
                 </button>
@@ -28,7 +28,7 @@
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                 </button>
                 </button>
             {% endif %}
             {% endif %}
-            {% if perms.dcim.delete_consoleport %}
+            {% if 'bulk_delete' in actions %}
                 <button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-danger btn-sm">
                 <button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-danger btn-sm">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 </button>
                 </button>

+ 2 - 2
netbox/templates/dcim/device/consoleserverports.html

@@ -17,7 +17,7 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
         <div class="bulk-button-group">
-            {% if perms.dcim.change_consoleserverport %}
+            {% if 'bulk_edit' in actions %}
                 <button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                 <button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
                 </button>
@@ -28,7 +28,7 @@
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                 </button>
                 </button>
             {% endif %}
             {% endif %}
-            {% if perms.dcim.delete_consoleserverport %}
+            {% if 'bulk_delete' in actions %}
                 <button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-danger btn-sm">
                 <button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-danger btn-sm">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 </button>
                 </button>

+ 2 - 2
netbox/templates/dcim/device/devicebays.html

@@ -17,7 +17,7 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
         <div class="bulk-button-group">
-            {% if perms.dcim.change_devicebay %}
+            {% if 'bulk_edit' in actions %}
                 <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                 <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
                 </button>
@@ -25,7 +25,7 @@
                     <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
                     <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
                 </button>
                 </button>
             {% endif %}
             {% endif %}
-            {% if perms.dcim.delete_devicebay %}
+            {% if 'bulk_delete' in actions %}
                 <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
                 <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete selected
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete selected
                 </button>
                 </button>

+ 2 - 2
netbox/templates/dcim/device/frontports.html

@@ -17,7 +17,7 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
         <div class="bulk-button-group">
-            {% if perms.dcim.change_frontport %}
+            {% if 'bulk_edit' in actions %}
                 <button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                 <button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
                 </button>
@@ -28,7 +28,7 @@
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                 </button>
                 </button>
             {% endif %}
             {% endif %}
-            {% if perms.dcim.delete_frontport %}
+            {% if 'bulk_delete' in actions %}
                 <button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-danger btn-sm">
                 <button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-danger btn-sm">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 </button>
                 </button>

+ 2 - 2
netbox/templates/dcim/device/interfaces.html

@@ -53,7 +53,7 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
         <div class="bulk-button-group">
-        {% if perms.dcim.change_interface %}
+          {% if 'bulk_edit' in actions %}
             <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
             <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                 <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
             </button>
             </button>
@@ -64,7 +64,7 @@
                 <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                 <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
             </button>
             </button>
         {% endif %}
         {% endif %}
-        {% if perms.dcim.delete_interface %}
+          {% if 'bulk_delete' in actions %}
             <button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-danger btn-sm">
             <button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-danger btn-sm">
                 <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
             </button>
             </button>

+ 2 - 2
netbox/templates/dcim/device/inventory.html

@@ -17,7 +17,7 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
         <div class="bulk-button-group">
-            {% if perms.dcim.change_inventoryitem %}
+            {% if 'bulk_edit' in actions %}
                 <button type="submit" name="_rename" formaction="{% url 'dcim:inventoryitem_bulk_rename' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-warning btn-sm">
                 <button type="submit" name="_rename" formaction="{% url 'dcim:inventoryitem_bulk_rename' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-warning btn-sm">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
                 </button>
@@ -25,7 +25,7 @@
                     <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
                     <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
                 </button>
                 </button>
             {% endif %}
             {% endif %}
-            {% if perms.dcim.delete_inventoryitem %}
+            {% if 'bulk_delete' in actions %}
                 <button type="submit" name="_delete" formaction="{% url 'dcim:inventoryitem_bulk_delete' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-danger btn-sm">
                 <button type="submit" name="_delete" formaction="{% url 'dcim:inventoryitem_bulk_delete' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-danger btn-sm">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 </button>
                 </button>

+ 2 - 2
netbox/templates/dcim/device/modulebays.html

@@ -17,7 +17,7 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
         <div class="bulk-button-group">
-            {% if perms.dcim.change_modulebay %}
+            {% if 'bulk_edit' in actions %}
                 <button type="submit" name="_rename" formaction="{% url 'dcim:modulebay_bulk_rename' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                 <button type="submit" name="_rename" formaction="{% url 'dcim:modulebay_bulk_rename' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
                 </button>
@@ -25,7 +25,7 @@
                     <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
                     <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
                 </button>
                 </button>
             {% endif %}
             {% endif %}
-            {% if perms.dcim.delete_modulebay %}
+            {% if 'bulk_delete' in actions %}
                 <button type="submit" formaction="{% url 'dcim:modulebay_bulk_delete' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
                 <button type="submit" formaction="{% url 'dcim:modulebay_bulk_delete' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete selected
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete selected
                 </button>
                 </button>

+ 2 - 2
netbox/templates/dcim/device/poweroutlets.html

@@ -17,7 +17,7 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
         <div class="bulk-button-group">
-            {% if perms.dcim.change_powerport %}
+            {% if 'bulk_edit' in actions %}
                 <button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                 <button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
                 </button>
@@ -28,7 +28,7 @@
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                 </button>
                 </button>
             {% endif %}
             {% endif %}
-            {% if perms.dcim.delete_poweroutlet %}
+            {% if 'bulk_delete' in actions %}
                 <button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-danger btn-sm">
                 <button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-danger btn-sm">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 </button>
                 </button>

+ 2 - 2
netbox/templates/dcim/device/powerports.html

@@ -17,7 +17,7 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
         <div class="bulk-button-group">
-            {% if perms.dcim.change_powerport %}
+            {% if 'bulk_edit' in actions %}
                 <button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                 <button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
                 </button>
@@ -28,7 +28,7 @@
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                 </button>
                 </button>
             {% endif %}
             {% endif %}
-            {% if perms.dcim.delete_powerport %}
+            {% if 'bulk_delete' in actions %}
                 <button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-danger btn-sm">
                 <button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-danger btn-sm">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 </button>
                 </button>

+ 2 - 2
netbox/templates/dcim/device/rearports.html

@@ -17,7 +17,7 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
         <div class="bulk-button-group">
-            {% if perms.dcim.change_rearport %}
+            {% if 'bulk_edit' in actions %}
                 <button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                 <button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
                 </button>
@@ -28,7 +28,7 @@
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                 </button>
                 </button>
             {% endif %}
             {% endif %}
-            {% if perms.dcim.delete_rearport %}
+            {% if 'bulk_delete' in actions %}
                 <button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-danger btn-sm">
                 <button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-danger btn-sm">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 </button>
                 </button>

+ 2 - 2
netbox/templates/ipam/aggregate/prefixes.html

@@ -25,12 +25,12 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
       <div class="bulk-button-group">
       <div class="bulk-button-group">
-        {% if perms.ipam.change_prefix %}
+        {% if 'bulk_edit' in actions %}
           <button type="submit" name="_edit" formaction="{% url 'ipam:prefix_bulk_edit' %}?return_url={% url 'ipam:aggregate_prefixes' pk=object.pk %}" class="btn btn-warning btn-sm">
           <button type="submit" name="_edit" formaction="{% url 'ipam:prefix_bulk_edit' %}?return_url={% url 'ipam:aggregate_prefixes' pk=object.pk %}" class="btn btn-warning btn-sm">
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
           </button>
           </button>
         {% endif %}
         {% endif %}
-        {% if perms.ipam.delete_prefix %}
+        {% if 'bulk_delete' in actions %}
           <button type="submit" name="_delete" formaction="{% url 'ipam:prefix_bulk_delete' %}?return_url={% url 'ipam:aggregate_prefixes' pk=object.pk %}" class="btn btn-danger btn-sm">
           <button type="submit" name="_delete" formaction="{% url 'ipam:prefix_bulk_delete' %}?return_url={% url 'ipam:aggregate_prefixes' pk=object.pk %}" class="btn btn-danger btn-sm">
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
           </button>
           </button>

+ 2 - 2
netbox/templates/ipam/iprange/ip_addresses.html

@@ -23,12 +23,12 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
       <div class="bulk-button-group">
       <div class="bulk-button-group">
-        {% if perms.ipam.change_ipaddress %}
+        {% if 'bulk_edit' in actions %}
           <button type="submit" name="_edit" formaction="{% url 'ipam:ipaddress_bulk_edit' %}?return_url={% url 'ipam:iprange_ipaddresses' pk=object.pk %}" class="btn btn-warning btn-sm">
           <button type="submit" name="_edit" formaction="{% url 'ipam:ipaddress_bulk_edit' %}?return_url={% url 'ipam:iprange_ipaddresses' pk=object.pk %}" class="btn btn-warning btn-sm">
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
           </button>
           </button>
         {% endif %}
         {% endif %}
-        {% if perms.ipam.delete_ipaddress %}
+        {% if 'bulk_delete' in actions %}
           <button type="submit" name="_delete" formaction="{% url 'ipam:ipaddress_bulk_delete' %}?return_url={% url 'ipam:iprange_ipaddresses' pk=object.pk %}" class="btn btn-danger btn-sm">
           <button type="submit" name="_delete" formaction="{% url 'ipam:ipaddress_bulk_delete' %}?return_url={% url 'ipam:iprange_ipaddresses' pk=object.pk %}" class="btn btn-danger btn-sm">
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
           </button>
           </button>

+ 2 - 2
netbox/templates/ipam/prefix/ip_addresses.html

@@ -23,12 +23,12 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
       <div class="bulk-button-group">
       <div class="bulk-button-group">
-        {% if perms.ipam.change_ipaddress %}
+        {% if 'bulk_edit' in actions %}
           <button type="submit" name="_edit" formaction="{% url 'ipam:ipaddress_bulk_edit' %}?return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-warning btn-sm">
           <button type="submit" name="_edit" formaction="{% url 'ipam:ipaddress_bulk_edit' %}?return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-warning btn-sm">
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
           </button>
           </button>
         {% endif %}
         {% endif %}
-        {% if perms.ipam.delete_ipaddress %}
+        {% if 'bulk_delete' in actions %}
           <button type="submit" name="_delete" formaction="{% url 'ipam:ipaddress_bulk_delete' %}?return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-danger btn-sm">
           <button type="submit" name="_delete" formaction="{% url 'ipam:ipaddress_bulk_delete' %}?return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-danger btn-sm">
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
           </button>
           </button>

+ 2 - 2
netbox/templates/ipam/prefix/ip_ranges.html

@@ -23,12 +23,12 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
       <div class="bulk-button-group">
       <div class="bulk-button-group">
-        {% if perms.ipam.change_iprange %}
+        {% if 'bulk_edit' in actions %}
           <button type="submit" name="_edit" formaction="{% url 'ipam:iprange_bulk_edit' %}?return_url={% url 'ipam:prefix_ipranges' pk=object.pk %}" class="btn btn-warning btn-sm">
           <button type="submit" name="_edit" formaction="{% url 'ipam:iprange_bulk_edit' %}?return_url={% url 'ipam:prefix_ipranges' pk=object.pk %}" class="btn btn-warning btn-sm">
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
           </button>
           </button>
         {% endif %}
         {% endif %}
-        {% if perms.ipam.delete_iprange %}
+        {% if 'bulk_delete' in actions %}
           <button type="submit" name="_delete" formaction="{% url 'ipam:iprange_bulk_delete' %}?return_url={% url 'ipam:prefix_ipranges' pk=object.pk %}" class="btn btn-danger btn-sm">
           <button type="submit" name="_delete" formaction="{% url 'ipam:iprange_bulk_delete' %}?return_url={% url 'ipam:prefix_ipranges' pk=object.pk %}" class="btn btn-danger btn-sm">
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
           </button>
           </button>

+ 2 - 2
netbox/templates/ipam/prefix/prefixes.html

@@ -25,12 +25,12 @@
 
 
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
       <div class="bulk-button-group">
       <div class="bulk-button-group">
-        {% if perms.ipam.change_prefix %}
+        {% if 'bulk_edit' in actions %}
           <button type="submit" name="_edit" formaction="{% url 'ipam:prefix_bulk_edit' %}?return_url={% url 'ipam:prefix_prefixes' pk=object.pk %}" class="btn btn-warning btn-sm">
           <button type="submit" name="_edit" formaction="{% url 'ipam:prefix_bulk_edit' %}?return_url={% url 'ipam:prefix_prefixes' pk=object.pk %}" class="btn btn-warning btn-sm">
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
           </button>
           </button>
         {% endif %}
         {% endif %}
-        {% if perms.ipam.delete_prefix %}
+        {% if 'bulk_delete' in actions %}
           <button type="submit" name="_delete" formaction="{% url 'ipam:prefix_bulk_delete' %}?return_url={% url 'ipam:prefix_prefixes' pk=object.pk %}" class="btn btn-danger btn-sm">
           <button type="submit" name="_delete" formaction="{% url 'ipam:prefix_bulk_delete' %}?return_url={% url 'ipam:prefix_prefixes' pk=object.pk %}" class="btn btn-danger btn-sm">
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
           </button>
           </button>

+ 2 - 2
netbox/templates/virtualization/cluster/virtual_machines.html

@@ -14,12 +14,12 @@
     </div>
     </div>
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
       <div class="bulk-button-group">
       <div class="bulk-button-group">
-        {% if perms.virtualization.change_virtualmachine %}
+        {% if 'bulk_edit' in actions %}
           <button type="submit" name="_edit" formaction="{% url 'virtualization:virtualmachine_bulk_edit' %}?return_url={% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="btn btn-warning btn-sm">
           <button type="submit" name="_edit" formaction="{% url 'virtualization:virtualmachine_bulk_edit' %}?return_url={% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="btn btn-warning btn-sm">
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
           </button>
           </button>
         {% endif %}
         {% endif %}
-        {% if perms.virtualization.delete_virtualmachine %}
+        {% if 'bulk_delete' in actions %}
           <button type="submit" name="_delete" formaction="{% url 'virtualization:virtualmachine_bulk_delete' %}?return_url={% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="btn btn-danger btn-sm">
           <button type="submit" name="_delete" formaction="{% url 'virtualization:virtualmachine_bulk_delete' %}?return_url={% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="btn btn-danger btn-sm">
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
           </button>
           </button>