Browse Source

Adds generic object children template (#13388)

* adds generic tab view template #12110

* Rename view_tab.html and move to generic/

* Fix console ports template

* Move bulk operations view resolution to template

* Avoid setting default template_name on ObjectChildrenView

* Move base_template and table_config context vars to base context

* removed bulk_delete_control from templates

* refactored bulk_controls view

* fixed table_config

* renamed object_tab.html to objectchildren_list.html

* removed unused import

* Refactor template blocks for bulk operation buttons

* Rename object children generic template

* Move disconnect bulk action into a separate template for device components

* Fix cluster devices & VM interfaces views

* minor button label change

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Abhimanyu Saharan 2 years ago
parent
commit
545769ad88
32 changed files with 331 additions and 1003 deletions
  1. 13 0
      netbox/dcim/views.py
  2. 4 6
      netbox/ipam/views.py
  3. 3 0
      netbox/netbox/views/generic/object_views.py
  4. 15 0
      netbox/templates/dcim/device/components_base.html
  5. 21 51
      netbox/templates/dcim/device/consoleports.html
  6. 21 51
      netbox/templates/dcim/device/consoleserverports.html
  7. 10 47
      netbox/templates/dcim/device/devicebays.html
  8. 21 51
      netbox/templates/dcim/device/frontports.html
  9. 22 61
      netbox/templates/dcim/device/interfaces.html
  10. 10 47
      netbox/templates/dcim/device/inventory.html
  11. 10 43
      netbox/templates/dcim/device/modulebays.html
  12. 21 51
      netbox/templates/dcim/device/poweroutlets.html
  13. 21 51
      netbox/templates/dcim/device/powerports.html
  14. 21 51
      netbox/templates/dcim/device/rearports.html
  15. 2 41
      netbox/templates/dcim/rack/non_racked_devices.html
  16. 9 40
      netbox/templates/dcim/rack/reservations.html
  17. 57 0
      netbox/templates/generic/object_children.html
  18. 2 37
      netbox/templates/ipam/aggregate/prefixes.html
  19. 0 36
      netbox/templates/ipam/asnrange/asns.html
  20. 0 19
      netbox/templates/ipam/ipaddress/ip_addresses.html
  21. 3 38
      netbox/templates/ipam/iprange/ip_addresses.html
  22. 2 37
      netbox/templates/ipam/prefix/ip_addresses.html
  23. 2 37
      netbox/templates/ipam/prefix/ip_ranges.html
  24. 2 38
      netbox/templates/ipam/prefix/prefixes.html
  25. 0 20
      netbox/templates/ipam/vlan/interfaces.html
  26. 0 20
      netbox/templates/ipam/vlan/vminterfaces.html
  27. 1 18
      netbox/templates/tenancy/object_contacts.html
  28. 10 27
      netbox/templates/virtualization/cluster/devices.html
  29. 0 35
      netbox/templates/virtualization/cluster/virtual_machines.html
  30. 10 44
      netbox/templates/virtualization/virtualmachine/interfaces.html
  31. 0 5
      netbox/tenancy/views.py
  32. 18 1
      netbox/virtualization/views.py

+ 13 - 0
netbox/dcim/views.py

@@ -1,4 +1,5 @@
 import traceback
+from collections import defaultdict
 
 from django.contrib import messages
 from django.contrib.contenttypes.models import ContentType
@@ -45,6 +46,15 @@ CABLE_TERMINATION_TYPES = {
 
 
 class DeviceComponentsView(generic.ObjectChildrenView):
+    actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename', 'bulk_disconnect')
+    action_perms = defaultdict(set, **{
+        'add': {'add'},
+        'import': {'add'},
+        'bulk_edit': {'change'},
+        'bulk_delete': {'delete'},
+        'bulk_rename': {'change'},
+        'bulk_disconnect': {'change'},
+    })
     queryset = Device.objects.all()
 
     def get_children(self, request, parent):
@@ -1997,6 +2007,7 @@ class DeviceModuleBaysView(DeviceComponentsView):
     table = tables.DeviceModuleBayTable
     filterset = filtersets.ModuleBayFilterSet
     template_name = 'dcim/device/modulebays.html'
+    actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
     tab = ViewTab(
         label=_('Module Bays'),
         badge=lambda obj: obj.modulebays.count(),
@@ -2012,6 +2023,7 @@ class DeviceDeviceBaysView(DeviceComponentsView):
     table = tables.DeviceDeviceBayTable
     filterset = filtersets.DeviceBayFilterSet
     template_name = 'dcim/device/devicebays.html'
+    actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
     tab = ViewTab(
         label=_('Device Bays'),
         badge=lambda obj: obj.devicebays.count(),
@@ -2023,6 +2035,7 @@ class DeviceDeviceBaysView(DeviceComponentsView):
 
 @register_model_view(Device, 'inventory')
 class DeviceInventoryView(DeviceComponentsView):
+    actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
     child_model = InventoryItem
     table = tables.DeviceInventoryItemTable
     filterset = filtersets.InventoryItemFilterSet

+ 4 - 6
netbox/ipam/views.py

@@ -216,7 +216,7 @@ class ASNRangeASNsView(generic.ObjectChildrenView):
     child_model = ASN
     table = tables.ASNTable
     filterset = filtersets.ASNFilterSet
-    template_name = 'ipam/asnrange/asns.html'
+    template_name = 'generic/object_children.html'
     tab = ViewTab(
         label=_('ASNs'),
         badge=lambda x: x.get_child_asns().count(),
@@ -816,7 +816,6 @@ class IPAddressAssignView(generic.ObjectView):
         table = None
 
         if form.is_valid():
-
             addresses = self.queryset.prefetch_related('vrf', 'tenant')
             # Limit to 100 results
             addresses = filtersets.IPAddressFilterSet(request.POST, addresses).qs[:100]
@@ -866,7 +865,7 @@ class IPAddressRelatedIPsView(generic.ObjectChildrenView):
     child_model = IPAddress
     table = tables.IPAddressTable
     filterset = filtersets.IPAddressFilterSet
-    template_name = 'ipam/ipaddress/ip_addresses.html'
+    template_name = 'generic/object_children.html'
     tab = ViewTab(
         label=_('Related IPs'),
         badge=lambda x: x.get_related_ips().count(),
@@ -963,7 +962,6 @@ class FHRPGroupView(generic.ObjectView):
     queryset = FHRPGroup.objects.all()
 
     def get_extra_context(self, request, instance):
-
         # Get assigned interfaces
         members_table = tables.FHRPGroupAssignmentTable(
             data=FHRPGroupAssignment.objects.restrict(request.user, 'view').filter(group=instance),
@@ -1077,7 +1075,7 @@ class VLANInterfacesView(generic.ObjectChildrenView):
     child_model = Interface
     table = tables.VLANDevicesTable
     filterset = InterfaceFilterSet
-    template_name = 'ipam/vlan/interfaces.html'
+    template_name = 'generic/object_children.html'
     tab = ViewTab(
         label=_('Device Interfaces'),
         badge=lambda x: x.get_interfaces().count(),
@@ -1095,7 +1093,7 @@ class VLANVMInterfacesView(generic.ObjectChildrenView):
     child_model = VMInterface
     table = tables.VLANVirtualMachinesTable
     filterset = VMInterfaceFilterSet
-    template_name = 'ipam/vlan/vminterfaces.html'
+    template_name = 'generic/object_children.html'
     tab = ViewTab(
         label=_('VM Interfaces'),
         badge=lambda x: x.get_vminterfaces().count(),

+ 3 - 0
netbox/netbox/views/generic/object_views.py

@@ -143,9 +143,12 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
         return render(request, self.get_template_name(), {
             'object': instance,
             'child_model': self.child_model,
+            'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
             'table': table,
+            'table_config': f'{table.name}_config',
             'actions': actions,
             'tab': self.tab,
+            'return_url': request.get_full_path(),
             **self.get_extra_context(request, instance),
         })
 

+ 15 - 0
netbox/templates/dcim/device/components_base.html

@@ -0,0 +1,15 @@
+{% extends 'generic/object_children.html' %}
+{% load helpers %}
+
+{% block bulk_edit_controls %}
+    {{ block.super }}
+    {% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
+        {% if 'bulk_rename' in actions and bulk_rename_view %}
+            <button type="submit" name="_rename"
+                    formaction="{% url bulk_rename_view %}?return_url={{ return_url }}"
+                    class="btn btn-outline-warning btn-sm">
+                <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+            </button>
+        {% endif %}
+    {% endwith %}
+{% endblock bulk_edit_controls %}

+ 21 - 51
netbox/templates/dcim/device/consoleports.html

@@ -1,57 +1,27 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
+{% extends 'dcim/device/components_base.html' %}
 {% load helpers %}
-{% load static %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsolePortTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+{% block bulk_delete_controls %}
+    {{ block.super }}
+    {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
+        {% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
+            <button type="submit" name="_disconnect"
+                    formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
+                    class="btn btn-outline-danger btn-sm">
+                <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
             </button>
-            <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>
-          </div>
         {% endif %}
-        <div class="btn-group" role="group">
-          {% 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>
-          {% endif %}
-          {% if 'bulk_edit' in actions %}
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-              <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-          {% endif %}
-        </div>
-      </div>
-      {% if perms.dcim.add_consoleport %}
+    {% endwith %}
+{% endblock bulk_delete_controls %}
+
+{% block bulk_extra_controls %}
+    {{ block.super }}
+    {% if perms.dcim.add_consoleport %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-sm btn-primary">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Port
-          </a>
+            <a href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Ports
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+    {% endif %}
+{% endblock bulk_extra_controls %}

+ 21 - 51
netbox/templates/dcim/device/consoleserverports.html

@@ -1,57 +1,27 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
+{% extends 'dcim/device/components_base.html' %}
 {% load helpers %}
-{% load static %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsoleServerPortTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+{% block bulk_delete_controls %}
+    {{ block.super }}
+    {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
+        {% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
+            <button type="submit" name="_disconnect"
+                    formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
+                    class="btn btn-outline-danger btn-sm">
+                <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
             </button>
-            <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>
-          </div>
         {% endif %}
-        <div class="btn-group" role="group">
-          {% 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>
-          {% endif %}
-          {% if 'bulk_edit' in actions %}
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-              <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-          {% endif %}
-        </div>
-      </div>
-      {% if perms.dcim.add_consoleserverport %}
+    {% endwith %}
+{% endblock bulk_delete_controls %}
+
+{% block bulk_extra_controls %}
+    {{ block.super }}
+    {% if perms.dcim.add_consoleserverport %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Server Ports
-          </a>
+            <a href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Server Ports
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+    {% endif %}
+{% endblock bulk_extra_controls %}

+ 10 - 47
netbox/templates/dcim/device/devicebays.html

@@ -1,50 +1,13 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load static %}
+{% extends 'dcim/device/components_base.html' %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceDeviceBayTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:devicebay_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <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>
-          </div>
-        {% endif %}
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" name="_delete" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-      {% if perms.dcim.add_devicebay %}
+{% block bulk_extra_controls %}
+    {{ block.super }}
+    {% if perms.dcim.add_devicebay %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Device Bays
-          </a>
+            <a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Device Bays
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+    {% endif %}
+{% endblock bulk_extra_controls %}

+ 21 - 51
netbox/templates/dcim/device/frontports.html

@@ -1,57 +1,27 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
+{% extends 'dcim/device/components_base.html' %}
 {% load helpers %}
-{% load static %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceFrontPortTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+{% block bulk_delete_controls %}
+    {{ block.super }}
+    {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
+        {% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
+            <button type="submit" name="_disconnect"
+                    formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
+                    class="btn btn-outline-danger btn-sm">
+                <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
             </button>
-            <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>
-          </div>
         {% endif %}
-        <div class="btn-group" role="group">
-          {% 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>
-          {% endif %}
-          {% if 'bulk_edit' in actions %}
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-              <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-          {% endif %}
-        </div>
-      </div>
-      {% if perms.dcim.add_frontport %}
+    {% endwith %}
+{% endblock bulk_delete_controls %}
+
+{% block bulk_extra_controls %}
+    {{ block.super }}
+    {% if perms.dcim.add_frontport %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add front ports
-          </a>
+            <a href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add front ports
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+    {% endif %}
+{% endblock bulk_extra_controls %}

+ 22 - 61
netbox/templates/dcim/device/interfaces.html

@@ -1,66 +1,27 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
+{% extends 'dcim/device/components_base.html' %}
 {% load helpers %}
-{% load static %}
 
-{% block content %}
-  {% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %}
-
-<form method="post">
-  {% csrf_token %}
-
-  <div class="card">
-    <div class="card-body htmx-container table-responsive" id="object_list">
-      {% include 'htmx/table.html' %}
-    </div>
-  </div>
-
-  <div class="noprint bulk-buttons">
-    <div class="bulk-button-group">
-      {% if 'bulk_edit' in actions %}
-        <div class="btn-group" role="group">
-          <button type="submit" name="_edit"
-            formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
-            class="btn btn-warning btn-sm">
-            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-          </button>
-          <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>
-        </div>
-      {% endif %}
-      <div class="btn-group" role="group">
-        {% 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>
+{% block bulk_delete_controls %}
+    {{ block.super }}
+    {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
+        {% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
+            <button type="submit" name="_disconnect"
+                    formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
+                    class="btn btn-outline-danger btn-sm">
+                <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+            </button>
         {% endif %}
-        {% if 'bulk_edit' in actions %}
-          <button type="submit" name="_disconnect"
-            formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
-            class="btn btn-outline-danger btn-sm">
-            <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-          </button>
-        {% endif %}
-      </div>
-    </div>
+    {% endwith %}
+{% endblock bulk_delete_controls %}
+
+{% block bulk_extra_controls %}
+    {{ block.super }}
     {% if perms.dcim.add_interface %}
-      <div class="bulk-button-group">
-        <a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
-          class="btn btn-primary btn-sm">
-          <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Interfaces
-        </a>
-      </div>
+        <div class="bulk-button-group">
+            <a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Interfaces
+            </a>
+        </div>
     {% endif %}
-  </div>
-</form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+{% endblock bulk_extra_controls %}

+ 10 - 47
netbox/templates/dcim/device/inventory.html

@@ -1,50 +1,13 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load static %}
+{% extends 'dcim/device/components_base.html' %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceInventoryItemTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:inventoryitem_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_rename" formaction="{% url 'dcim:inventoryitem_bulk_rename' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-              <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-          </div>
-        {% endif %}
-        {% 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>
-        {% endif %}
-      </div>
-      {% if perms.dcim.add_inventoryitem %}
+{% block bulk_extra_controls %}
+    {{ block.super }}
+    {% if perms.dcim.add_inventoryitem %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Inventory Item
-          </a>
+            <a href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Inventory Item
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+    {% endif %}
+{% endblock bulk_extra_controls %}

+ 10 - 43
netbox/templates/dcim/device/modulebays.html

@@ -1,46 +1,13 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load static %}
+{% extends 'dcim/device/components_base.html' %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceModuleBayTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:modulebay_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <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>
-          </div>
-        {% endif %}
-        {% 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-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-      {% if perms.dcim.add_modulebay %}
+{% block bulk_extra_controls %}
+    {{ block.super }}
+    {% if perms.dcim.add_modulebay %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Module Bays
-          </a>
+            <a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Module Bays
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-  {% table_config_form table %}
-{% endblock %}
+    {% endif %}
+{% endblock bulk_extra_controls %}

+ 21 - 51
netbox/templates/dcim/device/poweroutlets.html

@@ -1,57 +1,27 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
+{% extends 'dcim/device/components_base.html' %}
 {% load helpers %}
-{% load static %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerOutletTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+{% block bulk_delete_controls %}
+    {{ block.super }}
+    {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
+        {% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
+            <button type="submit" name="_disconnect"
+                    formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
+                    class="btn btn-outline-danger btn-sm">
+                <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
             </button>
-            <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>
-          </div>
         {% endif %}
-        <div class="btn-group" role="group">
-          {% 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>
-          {% endif %}
-          {% if 'bulk_edit' in actions %}
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-              <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-          {% endif %}
-        </div>
-      </div>
-      {% if perms.dcim.add_poweroutlet %}
+    {% endwith %}
+{% endblock bulk_delete_controls %}
+
+{% block bulk_extra_controls %}
+    {{ block.super }}
+    {% if perms.dcim.add_poweroutlet %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Outlets
-          </a>
+            <a href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Outlets
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+    {% endif %}
+{% endblock bulk_extra_controls %}

+ 21 - 51
netbox/templates/dcim/device/powerports.html

@@ -1,57 +1,27 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
+{% extends 'dcim/device/components_base.html' %}
 {% load helpers %}
-{% load static %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerPortTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+{% block bulk_delete_controls %}
+    {{ block.super }}
+    {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
+        {% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
+            <button type="submit" name="_disconnect"
+                    formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
+                    class="btn btn-outline-danger btn-sm">
+                <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
             </button>
-            <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>
-          </div>
         {% endif %}
-        <div class="btn-group" role="group">
-          {% 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>
-          {% endif %}
-          {% if 'bulk_edit' in actions %}
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-              <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-          {% endif %}
-        </div>
-      </div>
-      {% if perms.dcim.add_powerport %}
+    {% endwith %}
+{% endblock bulk_delete_controls %}
+
+{% block bulk_extra_controls %}
+    {{ block.super }}
+    {% if perms.dcim.add_powerport %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-sm btn-primary">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Port
-          </a>
+            <a href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}"
+               class="btn btn-sm btn-primary">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Port
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+    {% endif %}
+{% endblock bulk_extra_controls %}

+ 21 - 51
netbox/templates/dcim/device/rearports.html

@@ -1,57 +1,27 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
-{% load static %}
+{% extends 'dcim/device/components_base.html' %}
 {% load helpers %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceRearPortTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+{% block bulk_delete_controls %}
+    {{ block.super }}
+    {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
+        {% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
+            <button type="submit" name="_disconnect"
+                    formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
+                    class="btn btn-outline-danger btn-sm">
+                <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
             </button>
-            <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>
-          </div>
         {% endif %}
-        <div class="btn-group" role="group">
-          {% 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>
-          {% endif %}
-          {% if 'bulk_edit' in actions %}
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-              <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-          {% endif %}
-        </div>
-      </div>
-      {% if perms.dcim.add_rearport %}
+    {% endwith %}
+{% endblock bulk_delete_controls %}
+
+{% block bulk_extra_controls %}
+    {{ block.super }}
+    {% if perms.dcim.add_rearport %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add rear ports
-          </a>
+            <a href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add rear ports
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+    {% endif %}
+{% endblock bulk_extra_controls %}

+ 2 - 41
netbox/templates/dcim/rack/non_racked_devices.html

@@ -1,5 +1,4 @@
-{% extends 'dcim/rack/base.html' %}
-{% load helpers %}
+{% extends 'generic/object_children.html' %}
 
 {% block extra_controls %}
     {% if perms.dcim.add_device %}
@@ -10,42 +9,4 @@
             </a>
         </div>
     {% endif %}
-{% endblock %}
-
-{% block content %}
-    {% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
-
-    <form method="post">
-        {% csrf_token %}
-
-        <div class="card">
-            <div class="card-body htmx-container table-responsive" id="object_list">
-                {% include 'htmx/table.html' %}
-            </div>
-        </div>
-
-        <div class="noprint bulk-buttons">
-            <div class="bulk-button-group">
-                {% if 'bulk_edit' in actions %}
-                    <button type="submit" name="_edit"
-                            formaction="{% url 'dcim:device_bulk_edit' %}?return_url={% url 'dcim:rack_nonracked_devices' pk=object.pk %}"
-                            class="btn btn-warning btn-sm">
-                        <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-                    </button>
-                {% endif %}
-                {% if 'bulk_delete' in actions %}
-                    <button type="submit"
-                            formaction="{% url 'dcim:device_bulk_delete' %}?return_url={% url 'dcim:rack_nonracked_devices' pk=object.pk %}"
-                            class="btn btn-danger btn-sm">
-                        <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-                    </button>
-                {% endif %}
-            </div>
-        </div>
-    </form>
-{% endblock content %}
-
-{% block modals %}
-    {{ block.super }}
-    {% table_config_form table %}
-{% endblock modals %}
+{% endblock extra_controls %}

+ 9 - 40
netbox/templates/dcim/rack/reservations.html

@@ -1,43 +1,12 @@
-{% extends 'dcim/rack/base.html' %}
-{% load helpers %}
+{% extends 'generic/object_children.html' %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="RackReservationTable_config" %}
-
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <button type="submit" name="_edit" formaction="{% url 'dcim:rackreservation_bulk_edit' %}?return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-warning btn-sm">
-            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-          </button>
-        {% endif %}
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" formaction="{% url 'dcim:rackreservation_bulk_delete' %}?return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-      {% if perms.dcim.add_rackreservation %}
+{% block extra_controls %}
+    {% if perms.dcim.add_rackreservation %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:rackreservation_add' %}?rack={{ object.pk }}&return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add reservation
-          </a>
+            <a href="{% url 'dcim:rackreservation_add' %}?rack={{ object.pk }}&return_url={% url 'dcim:rack_reservations' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add reservation
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+    {% endif %}
+{% endblock extra_controls %}

+ 57 - 0
netbox/templates/generic/object_children.html

@@ -0,0 +1,57 @@
+{% extends base_template %}
+{% load helpers %}
+
+{% block content %}
+    {% include 'inc/table_controls_htmx.html' with table_modal=table_config %}
+    <form method="post">
+        {% csrf_token %}
+        <div class="card">
+            <div class="card-body htmx-container table-responsive" id="object_list">
+                {% include 'htmx/table.html' %}
+            </div>
+        </div>
+        <div class="noprint bulk-buttons">
+            {% block bulk_controls %}
+                <div class="bulk-button-group">
+                    <div class="btn-group" role="group">
+                        {# Bulk edit buttons #}
+                        {% block bulk_edit_controls %}
+                            {% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
+                                {% if 'bulk_edit' in actions and bulk_edit_view %}
+                                    <button type="submit" name="_edit"
+                                            formaction="{% url bulk_edit_view %}?return_url={{ return_url }}"
+                                            class="btn btn-warning btn-sm">
+                                        <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
+                                    </button>
+                                {% endif %}
+                            {% endwith %}
+                        {% endblock bulk_edit_controls %}
+                    </div>
+                    <div class="btn-group" role="group">
+                        {# Bulk delete buttons #}
+                        {% block bulk_delete_controls %}
+                            {% with bulk_delete_view=child_model|validated_viewname:"bulk_delete" %}
+                                {% if 'bulk_delete' in actions and bulk_delete_view %}
+                                    <button type="submit"
+                                            formaction="{% url bulk_delete_view %}?return_url={{ return_url }}"
+                                            class="btn btn-danger btn-sm">
+                                        <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete Selected
+                                    </button>
+                                {% endif %}
+                            {% endwith %}
+                        {% endblock bulk_delete_controls %}
+                    </div>
+                </div>
+                <div class="bulk-button-group">
+                    {# Other bulk action buttons #}
+                    {% block bulk_extra_controls %}{% endblock %}
+                </div>
+            {% endblock bulk_controls %}
+        </div>
+    </form>
+{% endblock content %}
+
+{% block modals %}
+    {{ block.super }}
+    {% table_config_form table %}
+{% endblock modals %}

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

@@ -1,5 +1,4 @@
-{% extends 'ipam/aggregate/base.html' %}
-{% load helpers %}
+{% extends 'generic/object_children.html' %}
 
 {% block extra_controls %}
   {% include 'ipam/inc/toggle_available.html' %}
@@ -9,38 +8,4 @@
     </a>
   {% endif %}
   {{ block.super }}
-{% endblock %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% 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 '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>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+{% endblock extra_controls %}

+ 0 - 36
netbox/templates/ipam/asnrange/asns.html

@@ -1,36 +0,0 @@
-{% extends 'ipam/asnrange/base.html' %}
-{% load helpers %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="ASNTable_config" %}
-
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <button type="submit" name="_edit" formaction="{% url 'ipam:asn_bulk_edit' %}?return_url={% url 'ipam:asnrange_asns' pk=object.pk %}" class="btn btn-warning btn-sm">
-            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-          </button>
-        {% endif %}
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" name="_delete" formaction="{% url 'ipam:asn_bulk_delete' %}?return_url={% url 'ipam:asnrange_asns' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}

+ 0 - 19
netbox/templates/ipam/ipaddress/ip_addresses.html

@@ -1,19 +0,0 @@
-{% extends 'ipam/ipaddress/base.html' %}
-{% load helpers %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
-  <form method="post">
-    {% csrf_token %}
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}

+ 3 - 38
netbox/templates/ipam/iprange/ip_addresses.html

@@ -1,44 +1,9 @@
-{% extends 'ipam/iprange/base.html' %}
-{% load helpers %}
+{% extends 'generic/object_children.html' %}
 
 {% block extra_controls %}
-  {% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and object.first_available_ip %}
+  {% if perms.ipam.add_ipaddress and object.first_available_ip %}
     <a href="{% url 'ipam:ipaddress_add' %}?address={{ object.first_available_ip }}&vrf={{ object.vrf.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-sm btn-primary">
         <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add IP Address
     </a>
   {% endif %}
-{% endblock %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% 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 '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>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+{% endblock extra_controls %}

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

@@ -1,5 +1,4 @@
-{% extends 'ipam/prefix/base.html' %}
-{% load helpers %}
+{% extends 'generic/object_children.html' %}
 
 {% block extra_controls %}
   {% if perms.ipam.add_ipaddress and first_available_ip %}
@@ -7,38 +6,4 @@
         <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add IP Address
     </a>
   {% endif %}
-{% endblock %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% 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 '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>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+{% endblock extra_controls %}

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

@@ -1,5 +1,4 @@
-{% extends 'ipam/prefix/base.html' %}
-{% load helpers %}
+{% extends 'generic/object_children.html' %}
 
 {% block extra_controls %}
   {% if perms.ipam.add_iprange and first_available_ip %}
@@ -7,38 +6,4 @@
         <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add IP Range
     </a>
   {% endif %}
-{% endblock %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="IPRangeTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% 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 '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>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+{% endblock extra_controls %}

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

@@ -1,5 +1,4 @@
-{% extends 'ipam/prefix/base.html' %}
-{% load helpers %}
+{% extends 'generic/object_children.html' %}
 
 {% block extra_controls %}
   {% include 'ipam/inc/toggle_available.html' %}
@@ -8,39 +7,4 @@
       <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Prefix
     </a>
   {% endif %}
-  {{ block.super }}
-{% endblock %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% 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 '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>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+{% endblock extra_controls %}

+ 0 - 20
netbox/templates/ipam/vlan/interfaces.html

@@ -1,20 +0,0 @@
-{% extends 'ipam/vlan/base.html' %}
-{% load helpers %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="VLANDevicesTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-  </form>
-{% endblock content %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}

+ 0 - 20
netbox/templates/ipam/vlan/vminterfaces.html

@@ -1,20 +0,0 @@
-{% extends 'ipam/vlan/base.html' %}
-{% load helpers %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="VLANVirtualMachinesTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-  </form>
-{% endblock content %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}

+ 1 - 18
netbox/templates/tenancy/object_contacts.html

@@ -1,4 +1,4 @@
-{% extends base_template %}
+{% extends 'generic/object_children.html' %}
 {% load helpers %}
 
 {% block extra_controls %}
@@ -10,20 +10,3 @@
     {% endwith %}
   {% endif %}
 {% endblock %}
-
-{% block content %}
-    {% include 'inc/table_controls_htmx.html' with table_modal="ContactAssignmentTable_config" %}
-    <form method="post">
-        {% csrf_token %}
-        <div class="card">
-            <div class="card-body" id="object_list">
-                {% include 'htmx/table.html' %}
-            </div>
-        </div>
-    </form>
-{% endblock content %}
-
-{% block modals %}
-    {{ block.super }}
-    {% table_config_form table %}
-{% endblock modals %}

+ 10 - 27
netbox/templates/virtualization/cluster/devices.html

@@ -1,30 +1,13 @@
-{% extends 'virtualization/cluster/base.html' %}
+{% extends 'generic/object_children.html' %}
 {% load helpers %}
-{% load render_table from django_tables2 %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
-  
-  <form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
-    {% csrf_token %}
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if perms.virtualization.change_cluster %}
-          <button type="submit" name="_remove" class="btn btn-danger btn-sm">
+{% block bulk_delete_controls %}
+    {{ block.super }}
+    {% if 'bulk_remove_devices' in actions %}
+        <button type="submit" name="_remove"
+                formaction="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}?return_url={{ return_url }}"
+                class="btn btn-danger btn-sm">
             <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Remove Devices
-          </button>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock content %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+        </button>
+    {% endif %}
+{% endblock bulk_delete_controls %}

+ 0 - 35
netbox/templates/virtualization/cluster/virtual_machines.html

@@ -1,35 +0,0 @@
-{% extends 'virtualization/cluster/base.html' %}
-{% load helpers %}
-{% load render_table from django_tables2 %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% 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 '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>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock content %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}

+ 10 - 44
netbox/templates/virtualization/virtualmachine/interfaces.html

@@ -1,47 +1,13 @@
-{% extends 'virtualization/virtualmachine/base.html' %}
-{% load render_table from django_tables2 %}
+{% extends 'generic/object_children.html' %}
 {% load helpers %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineVMInterfaceTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint">
-      {% if perms.virtualization.change_vminterface %}
-        <div class="btn-group" role="group">
-          <button type="submit" name="_edit" formaction="{% url 'virtualization:vminterface_bulk_edit' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-warning btn-sm">
-            <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
-          </button>
-          <button type="submit" name="_rename" formaction="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-            <span class="mdi mdi-pencil" aria-hidden="true"></span> Rename
-          </button>
-        </div>
-      {% endif %}
-      {% if perms.virtualization.delete_vminterface %}
-        <button type="submit" name="_delete" formaction="{% url 'virtualization:vminterface_bulk_delete' %}?return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-danger btn-sm">
-          <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete
+{% block bulk_edit_controls %}
+    {{ block.super }}
+    {% if 'bulk_rename' in actions %}
+        <button type="submit" name="_rename"
+                formaction="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={{ return_url }}"
+                class="btn btn-outline-warning btn-sm">
+            <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
         </button>
-      {% endif %}
-      {% if perms.virtualization.add_vminterface %}
-        <div class="float-end">
-          <a href="{% url 'virtualization:vminterface_add' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add interfaces
-          </a>
-        </div>
-      {% endif %}
-     </div>
-  </form>
-{% endblock content %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+    {% endif %}
+{% endblock bulk_edit_controls %}

+ 0 - 5
netbox/tenancy/views.py

@@ -41,11 +41,6 @@ class ObjectContactsView(generic.ObjectChildrenView):
 
         return table
 
-    def get_extra_context(self, request, instance):
-        return {
-            'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
-        }
-
 #
 # Tenant groups
 #

+ 18 - 1
netbox/virtualization/views.py

@@ -1,3 +1,5 @@
+from collections import defaultdict
+
 from django.contrib import messages
 from django.db import transaction
 from django.db.models import Prefetch, Sum
@@ -175,7 +177,7 @@ class ClusterVirtualMachinesView(generic.ObjectChildrenView):
     child_model = VirtualMachine
     table = tables.VirtualMachineTable
     filterset = filtersets.VirtualMachineFilterSet
-    template_name = 'virtualization/cluster/virtual_machines.html'
+    template_name = 'generic/object_children.html'
     tab = ViewTab(
         label=_('Virtual Machines'),
         badge=lambda obj: obj.virtual_machines.count(),
@@ -194,6 +196,13 @@ class ClusterDevicesView(generic.ObjectChildrenView):
     table = DeviceTable
     filterset = DeviceFilterSet
     template_name = 'virtualization/cluster/devices.html'
+    actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_remove_devices')
+    action_perms = defaultdict(set, **{
+        'add': {'add'},
+        'import': {'add'},
+        'bulk_edit': {'change'},
+        'bulk_remove_devices': {'change'},
+    })
     tab = ViewTab(
         label=_('Devices'),
         badge=lambda obj: obj.devices.count(),
@@ -353,6 +362,14 @@ class VirtualMachineInterfacesView(generic.ObjectChildrenView):
         permission='virtualization.view_vminterface',
         weight=500
     )
+    actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
+    action_perms = defaultdict(set, **{
+        'add': {'add'},
+        'import': {'add'},
+        'bulk_edit': {'change'},
+        'bulk_delete': {'delete'},
+        'bulk_rename': {'change'},
+    })
 
     def get_children(self, request, parent):
         return parent.interfaces.restrict(request.user, 'view').prefetch_related(