Преглед изворни кода

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

Closes #9092: Introduce support for use of ObjectChildrenView by plugins
Jeremy Stretch пре 3 година
родитељ
комит
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.
 
-| 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.

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

+ 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.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):

+ 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.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),
         })
 

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

@@ -17,7 +17,7 @@
 
     <div class="noprint bulk-buttons">
         <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">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
@@ -28,7 +28,7 @@
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                 </button>
             {% 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">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 </button>

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

@@ -17,7 +17,7 @@
 
     <div class="noprint bulk-buttons">
         <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">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
@@ -28,7 +28,7 @@
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                 </button>
             {% 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">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 </button>

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

@@ -17,7 +17,7 @@
 
     <div class="noprint bulk-buttons">
         <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">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
@@ -25,7 +25,7 @@
                     <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
                 </button>
             {% 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">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete selected
                 </button>

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

@@ -17,7 +17,7 @@
 
     <div class="noprint bulk-buttons">
         <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">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
@@ -28,7 +28,7 @@
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                 </button>
             {% 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">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 </button>

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

@@ -53,7 +53,7 @@
 
     <div class="noprint bulk-buttons">
         <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">
                 <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
             </button>
@@ -64,7 +64,7 @@
                 <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
             </button>
         {% 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">
                 <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
             </button>

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

@@ -17,7 +17,7 @@
 
     <div class="noprint bulk-buttons">
         <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">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
@@ -25,7 +25,7 @@
                     <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
                 </button>
             {% 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">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 </button>

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

@@ -17,7 +17,7 @@
 
     <div class="noprint bulk-buttons">
         <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">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
@@ -25,7 +25,7 @@
                     <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
                 </button>
             {% 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">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete selected
                 </button>

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

@@ -17,7 +17,7 @@
 
     <div class="noprint bulk-buttons">
         <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">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
@@ -28,7 +28,7 @@
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                 </button>
             {% 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">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 </button>

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

@@ -17,7 +17,7 @@
 
     <div class="noprint bulk-buttons">
         <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">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
@@ -28,7 +28,7 @@
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                 </button>
             {% 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">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 </button>

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

@@ -17,7 +17,7 @@
 
     <div class="noprint bulk-buttons">
         <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">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
@@ -28,7 +28,7 @@
                     <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
                 </button>
             {% 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">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                 </button>

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

@@ -25,12 +25,12 @@
 
     <div class="noprint bulk-buttons">
       <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">
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
           </button>
         {% 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">
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
           </button>

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

@@ -23,12 +23,12 @@
 
     <div class="noprint bulk-buttons">
       <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">
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
           </button>
         {% 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">
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
           </button>

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

@@ -23,12 +23,12 @@
 
     <div class="noprint bulk-buttons">
       <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">
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
           </button>
         {% 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">
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
           </button>

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

@@ -23,12 +23,12 @@
 
     <div class="noprint bulk-buttons">
       <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">
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
           </button>
         {% 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">
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
           </button>

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

@@ -25,12 +25,12 @@
 
     <div class="noprint bulk-buttons">
       <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">
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
           </button>
         {% 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">
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
           </button>

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

@@ -14,12 +14,12 @@
     </div>
     <div class="noprint bulk-buttons">
       <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">
             <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
           </button>
         {% 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">
             <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
           </button>