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

Adds contact tabs (#12460)

* adds contact tabs #11599

* fixed lint issues

* changes as per review

* changes as per review

* replaces generic object template with base template
Abhimanyu Saharan 2 лет назад
Родитель
Сommit
4eb5e90ccc

+ 16 - 1
netbox/circuits/views.py

@@ -1,10 +1,10 @@
 from django.contrib import messages
 from django.contrib import messages
 from django.db import transaction
 from django.db import transaction
-from django.db.models import Q
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 
 
 from dcim.views import PathTraceView
 from dcim.views import PathTraceView
 from netbox.views import generic
 from netbox.views import generic
+from tenancy.views import ObjectContactsView
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.utils import count_related
 from utilities.utils import count_related
 from utilities.views import register_model_view
 from utilities.views import register_model_view
@@ -73,6 +73,11 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
     table = tables.ProviderTable
     table = tables.ProviderTable
 
 
 
 
+@register_model_view(Provider, 'contacts')
+class ProviderContactsView(ObjectContactsView):
+    queryset = Provider.objects.all()
+
+
 #
 #
 # ProviderAccounts
 # ProviderAccounts
 #
 #
@@ -134,6 +139,11 @@ class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
     table = tables.ProviderAccountTable
     table = tables.ProviderAccountTable
 
 
 
 
+@register_model_view(ProviderAccount, 'contacts')
+class ProviderAccountContactsView(ObjectContactsView):
+    queryset = ProviderAccount.objects.all()
+
+
 #
 #
 # Provider networks
 # Provider networks
 #
 #
@@ -389,6 +399,11 @@ class CircuitSwapTerminations(generic.ObjectEditView):
         })
         })
 
 
 
 
+@register_model_view(Circuit, 'contacts')
+class CircuitContactsView(ObjectContactsView):
+    queryset = Circuit.objects.all()
+
+
 #
 #
 # Circuit terminations
 # Circuit terminations
 #
 #

+ 41 - 0
netbox/dcim/views.py

@@ -20,6 +20,7 @@ from extras.views import ObjectConfigContextView
 from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
 from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
 from ipam.tables import InterfaceVLANTable
 from ipam.tables import InterfaceVLANTable
 from netbox.views import generic
 from netbox.views import generic
+from tenancy.views import ObjectContactsView
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 from utilities.permissions import get_permission_for_model
 from utilities.permissions import get_permission_for_model
@@ -267,6 +268,11 @@ class RegionBulkDeleteView(generic.BulkDeleteView):
     table = tables.RegionTable
     table = tables.RegionTable
 
 
 
 
+@register_model_view(Region, 'contacts')
+class RegionContactsView(ObjectContactsView):
+    queryset = Region.objects.all()
+
+
 #
 #
 # Site groups
 # Site groups
 #
 #
@@ -342,6 +348,11 @@ class SiteGroupBulkDeleteView(generic.BulkDeleteView):
     table = tables.SiteGroupTable
     table = tables.SiteGroupTable
 
 
 
 
+@register_model_view(SiteGroup, 'contacts')
+class SiteGroupContactsView(ObjectContactsView):
+    queryset = SiteGroup.objects.all()
+
+
 #
 #
 # Sites
 # Sites
 #
 #
@@ -435,6 +446,11 @@ class SiteBulkDeleteView(generic.BulkDeleteView):
     table = tables.SiteTable
     table = tables.SiteTable
 
 
 
 
+@register_model_view(Site, 'contacts')
+class SiteContactsView(ObjectContactsView):
+    queryset = Site.objects.all()
+
+
 #
 #
 # Locations
 # Locations
 #
 #
@@ -523,6 +539,11 @@ class LocationBulkDeleteView(generic.BulkDeleteView):
     table = tables.LocationTable
     table = tables.LocationTable
 
 
 
 
+@register_model_view(Location, 'contacts')
+class LocationContactsView(ObjectContactsView):
+    queryset = Location.objects.all()
+
+
 #
 #
 # Rack roles
 # Rack roles
 #
 #
@@ -740,6 +761,11 @@ class RackBulkDeleteView(generic.BulkDeleteView):
     table = tables.RackTable
     table = tables.RackTable
 
 
 
 
+@register_model_view(Rack, 'contacts')
+class RackContactsView(ObjectContactsView):
+    queryset = Rack.objects.all()
+
+
 #
 #
 # Rack reservations
 # Rack reservations
 #
 #
@@ -874,6 +900,11 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView):
     table = tables.ManufacturerTable
     table = tables.ManufacturerTable
 
 
 
 
+@register_model_view(Manufacturer, 'contacts')
+class ManufacturerContactsView(ObjectContactsView):
+    queryset = Manufacturer.objects.all()
+
+
 #
 #
 # Device types
 # Device types
 #
 #
@@ -2088,6 +2119,11 @@ class DeviceBulkRenameView(generic.BulkRenameView):
     table = tables.DeviceTable
     table = tables.DeviceTable
 
 
 
 
+@register_model_view(Device, 'contacts')
+class DeviceContactsView(ObjectContactsView):
+    queryset = Device.objects.all()
+
+
 #
 #
 # Modules
 # Modules
 #
 #
@@ -3469,6 +3505,11 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView):
     table = tables.PowerPanelTable
     table = tables.PowerPanelTable
 
 
 
 
+@register_model_view(PowerPanel, 'contacts')
+class PowerPanelContactsView(ObjectContactsView):
+    queryset = PowerPanel.objects.all()
+
+
 #
 #
 # Power feeds
 # Power feeds
 #
 #

+ 6 - 0
netbox/ipam/views.py

@@ -9,6 +9,7 @@ from circuits.models import Provider
 from dcim.filtersets import InterfaceFilterSet
 from dcim.filtersets import InterfaceFilterSet
 from dcim.models import Interface, Site
 from dcim.models import Interface, Site
 from netbox.views import generic
 from netbox.views import generic
+from tenancy.views import ObjectContactsView
 from utilities.utils import count_related
 from utilities.utils import count_related
 from utilities.views import ViewTab, register_model_view
 from utilities.views import ViewTab, register_model_view
 from virtualization.filtersets import VMInterfaceFilterSet
 from virtualization.filtersets import VMInterfaceFilterSet
@@ -1300,6 +1301,11 @@ class L2VPNBulkDeleteView(generic.BulkDeleteView):
     table = tables.L2VPNTable
     table = tables.L2VPNTable
 
 
 
 
+@register_model_view(L2VPN, 'contacts')
+class L2VPNContactsView(ObjectContactsView):
+    queryset = L2VPN.objects.all()
+
+
 #
 #
 # L2VPN terminations
 # L2VPN terminations
 #
 #

+ 0 - 1
netbox/templates/circuits/circuit.html

@@ -70,7 +70,6 @@
     <div class="col col-md-6">
     <div class="col col-md-6">
       {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
       {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
       {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
       {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
-      {% include 'inc/panels/contacts.html' %}
       {% include 'inc/panels/image_attachments.html' %}
       {% include 'inc/panels/image_attachments.html' %}
       {% plugin_right_page object %}
       {% plugin_right_page object %}
     </div>
     </div>

+ 0 - 1
netbox/templates/circuits/provider.html

@@ -43,7 +43,6 @@
     <div class="col col-md-6">
     <div class="col col-md-6">
         {% include 'inc/panels/related_objects.html' %}
         {% include 'inc/panels/related_objects.html' %}
         {% include 'inc/panels/custom_fields.html' %}
         {% include 'inc/panels/custom_fields.html' %}
-        {% include 'inc/panels/contacts.html' %}
         {% plugin_right_page object %}
         {% plugin_right_page object %}
     </div>
     </div>
 </div>
 </div>

+ 0 - 1
netbox/templates/circuits/provideraccount.html

@@ -38,7 +38,6 @@
       {% include 'inc/panels/related_objects.html' %}
       {% include 'inc/panels/related_objects.html' %}
       {% include 'inc/panels/comments.html' %}
       {% include 'inc/panels/comments.html' %}
       {% include 'inc/panels/custom_fields.html' %}
       {% include 'inc/panels/custom_fields.html' %}
-      {% include 'inc/panels/contacts.html' %}
       {% plugin_right_page object %}
       {% plugin_right_page object %}
     </div>
     </div>
     <div class="col col-md-12">
     <div class="col col-md-12">

+ 0 - 1
netbox/templates/dcim/device.html

@@ -298,7 +298,6 @@
                 </div>
                 </div>
               {% endif %}
               {% endif %}
             </div>
             </div>
-            {% include 'inc/panels/contacts.html' %}
             {% include 'inc/panels/image_attachments.html' %}
             {% include 'inc/panels/image_attachments.html' %}
             <div class="card">
             <div class="card">
                 <h5 class="card-header">Dimensions</h5>
                 <h5 class="card-header">Dimensions</h5>

+ 0 - 1
netbox/templates/dcim/location.html

@@ -65,7 +65,6 @@
   </div>
   </div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">
     {% include 'inc/panels/related_objects.html' %}
     {% include 'inc/panels/related_objects.html' %}
-    {% include 'inc/panels/contacts.html' %}
     {% include 'dcim/inc/nonracked_devices.html' %}
     {% include 'dcim/inc/nonracked_devices.html' %}
     {% include 'inc/panels/image_attachments.html' %}
     {% include 'inc/panels/image_attachments.html' %}
     {% plugin_right_page object %}
     {% plugin_right_page object %}

+ 0 - 1
netbox/templates/dcim/manufacturer.html

@@ -51,7 +51,6 @@
 	<div class="col col-md-6">
 	<div class="col col-md-6">
     {% include 'inc/panels/related_objects.html' %}
     {% include 'inc/panels/related_objects.html' %}
     {% include 'inc/panels/custom_fields.html' %}
     {% include 'inc/panels/custom_fields.html' %}
-    {% include 'inc/panels/contacts.html' %}
     {% plugin_right_page object %}
     {% plugin_right_page object %}
   </div>
   </div>
 </div>
 </div>

+ 0 - 1
netbox/templates/dcim/powerpanel.html

@@ -40,7 +40,6 @@
 	<div class="col col-md-6">
 	<div class="col col-md-6">
     {% include 'inc/panels/related_objects.html' %}
     {% include 'inc/panels/related_objects.html' %}
     {% include 'inc/panels/custom_fields.html' %}
     {% include 'inc/panels/custom_fields.html' %}
-    {% include 'inc/panels/contacts.html' %}
     {% include 'inc/panels/image_attachments.html' %}
     {% include 'inc/panels/image_attachments.html' %}
     {% plugin_right_page object %}
     {% plugin_right_page object %}
   </div>
   </div>

+ 0 - 1
netbox/templates/dcim/rack.html

@@ -191,7 +191,6 @@
         </div>
         </div>
         {% include 'inc/panels/related_objects.html' %}
         {% include 'inc/panels/related_objects.html' %}
         {% include 'dcim/inc/nonracked_devices.html' %}
         {% include 'dcim/inc/nonracked_devices.html' %}
-        {% include 'inc/panels/contacts.html' %}
         {% plugin_right_page object %}
         {% plugin_right_page object %}
     </div>
     </div>
   </div>
   </div>

+ 0 - 1
netbox/templates/dcim/region.html

@@ -46,7 +46,6 @@
   </div>
   </div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">
     {% include 'inc/panels/related_objects.html' %}
     {% include 'inc/panels/related_objects.html' %}
-    {% include 'inc/panels/contacts.html' %}
     {% plugin_right_page object %}
     {% plugin_right_page object %}
 	</div>
 	</div>
 </div>
 </div>

+ 0 - 1
netbox/templates/dcim/site.html

@@ -131,7 +131,6 @@
     </div>
     </div>
     <div class="col col-md-6">
     <div class="col col-md-6">
       {% include 'inc/panels/related_objects.html' with filter_name='site_id' %}
       {% include 'inc/panels/related_objects.html' with filter_name='site_id' %}
-      {% include 'inc/panels/contacts.html' %}
       <div class="card">
       <div class="card">
         <h5 class="card-header">Locations</h5>
         <h5 class="card-header">Locations</h5>
         <div class='card-body'>
         <div class='card-body'>

+ 0 - 1
netbox/templates/dcim/sitegroup.html

@@ -42,7 +42,6 @@
     </div>
     </div>
     {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/custom_fields.html' %}
     {% include 'inc/panels/custom_fields.html' %}
-    {% include 'inc/panels/contacts.html' %}
     {% plugin_left_page object %}
     {% plugin_left_page object %}
   </div>
   </div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">

+ 0 - 63
netbox/templates/inc/panels/contacts.html

@@ -1,63 +0,0 @@
-{% load helpers %}
-
-<div class="card">
-  <h5 class="card-header">Contacts</h5>
-  <div class="card-body">
-    {% with contacts=object.contacts.all %}
-      {% if contacts.exists %}
-        <table class="table table-hover">
-          <tr>
-            <th>Name</th>
-            <th>Role</th>
-            <th>Priority</th>
-            <th>Phone</th>
-            <th>Email</th>
-            <th></th>
-          </tr>
-          {% for contact in contacts %}
-            <tr>
-              <td>{{ contact.contact|linkify }}</td>
-              <td>{{ contact.role|placeholder }}</td>
-              <td>{{ contact.get_priority_display|placeholder }}</td>
-              <td>
-                {% if contact.contact.phone %}
-                  <a href="tel:{{ contact.contact.phone }}">{{ contact.contact.phone }}</a>
-                {% else %}
-                  {{ ''|placeholder }}
-                {% endif %}
-              </td>
-              <td>
-                {% if contact.contact.email %}
-                  <a href="mailto:{{ contact.contact.email }}">{{ contact.contact.email }}</a>
-                {% else %}
-                  {{ ''|placeholder }}
-                {% endif %}
-              </td>
-              <td class="text-end noprint">
-                {% if perms.tenancy.change_contactassignment %}
-                  <a href="{% url 'tenancy:contactassignment_edit' pk=contact.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning btn-sm lh-1" title="Edit">
-                    <i class="mdi mdi-pencil" aria-hidden="true"></i>
-                  </a>
-                {% endif %}
-                {% if perms.tenancy.delete_contactassignment %}
-                  <a href="{% url 'tenancy:contactassignment_delete' pk=contact.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-danger btn-sm lh-1" title="Delete">
-                    <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
-                  </a>
-                {% endif %}
-              </td>
-            </tr>
-          {% endfor %}
-        </table>
-      {% else %}
-        <div class="text-muted">None</div>
-      {% endif %}
-    {% endwith %}
-  </div>
-  {% if perms.tenancy.add_contactassignment %}
-    <div class="card-footer text-end noprint">
-      <a href="{% url 'tenancy:contactassignment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
-        <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a contact
-      </a>
-    </div>
-  {% endif %}
-</div>

+ 0 - 1
netbox/templates/ipam/l2vpn.html

@@ -37,7 +37,6 @@
     {% plugin_left_page object %}
     {% plugin_left_page object %}
 	</div>
 	</div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">
-      {% include 'inc/panels/contacts.html' %}
       {% include 'inc/panels/custom_fields.html' %}
       {% include 'inc/panels/custom_fields.html' %}
       {% include 'inc/panels/comments.html' %}
       {% include 'inc/panels/comments.html' %}
       {% plugin_right_page object %}
       {% plugin_right_page object %}

+ 27 - 0
netbox/templates/tenancy/object_contacts.html

@@ -0,0 +1,27 @@
+{% extends base_template %}
+{% load helpers %}
+
+{% block extra_controls %}
+    {% if perms.tenancy.add_contactassignment %}
+    <a href="{% url 'tenancy:contactassignment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
+        <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a contact
+    </a>
+  {% endif %}
+{% endblock %}
+
+{% block content %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="ContactTable_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 %}

+ 0 - 1
netbox/templates/tenancy/tenant.html

@@ -30,7 +30,6 @@
       {% include 'inc/panels/custom_fields.html' %}
       {% include 'inc/panels/custom_fields.html' %}
       {% include 'inc/panels/tags.html' %}
       {% include 'inc/panels/tags.html' %}
       {% include 'inc/panels/comments.html' %}
       {% include 'inc/panels/comments.html' %}
-      {% include 'inc/panels/contacts.html' %}
       {% plugin_left_page object %}
       {% plugin_left_page object %}
     </div>
     </div>
     <div class="col col-md-5">
     <div class="col col-md-5">

+ 0 - 1
netbox/templates/virtualization/cluster.html

@@ -84,7 +84,6 @@
   </div>
   </div>
     {% include 'inc/panels/custom_fields.html' %}
     {% include 'inc/panels/custom_fields.html' %}
     {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/tags.html' %}
-    {% include 'inc/panels/contacts.html' %}
     {% plugin_right_page object %}
     {% plugin_right_page object %}
   </div>
   </div>
 </div>
 </div>

+ 0 - 1
netbox/templates/virtualization/clustergroup.html

@@ -37,7 +37,6 @@
 	<div class="col col-md-6">
 	<div class="col col-md-6">
     {% include 'inc/panels/related_objects.html' %}
     {% include 'inc/panels/related_objects.html' %}
     {% include 'inc/panels/custom_fields.html' %}
     {% include 'inc/panels/custom_fields.html' %}
-    {% include 'inc/panels/contacts.html' %}
     {% plugin_right_page object %}
     {% plugin_right_page object %}
   </div>
   </div>
 </div>
 </div>

+ 0 - 1
netbox/templates/virtualization/virtualmachine.html

@@ -158,7 +158,6 @@
             </div>
             </div>
           {% endif %}
           {% endif %}
         </div>
         </div>
-        {% include 'inc/panels/contacts.html' %}
         {% plugin_right_page object %}
         {% plugin_right_page object %}
     </div>
     </div>
 </div>
 </div>

+ 30 - 2
netbox/tenancy/views.py

@@ -7,17 +7,40 @@ from dcim.models import Cable, Device, Location, Rack, RackReservation, Site, Vi
 from ipam.models import Aggregate, ASN, IPAddress, IPRange, L2VPN, Prefix, VLAN, VRF
 from ipam.models import Aggregate, ASN, IPAddress, IPRange, L2VPN, Prefix, VLAN, VRF
 from netbox.views import generic
 from netbox.views import generic
 from utilities.utils import count_related
 from utilities.utils import count_related
-from utilities.views import register_model_view
+from utilities.views import register_model_view, ViewTab
 from virtualization.models import VirtualMachine, Cluster
 from virtualization.models import VirtualMachine, Cluster
 from wireless.models import WirelessLAN, WirelessLink
 from wireless.models import WirelessLAN, WirelessLink
 from . import filtersets, forms, tables
 from . import filtersets, forms, tables
 from .models import *
 from .models import *
 
 
 
 
+class ObjectContactsView(generic.ObjectChildrenView):
+    child_model = Contact
+    table = tables.ContactTable
+    filterset = filtersets.ContactFilterSet
+    template_name = 'tenancy/object_contacts.html'
+    tab = ViewTab(
+        label=_('Contacts'),
+        badge=lambda obj: obj.contacts.count(),
+        permission='tenancy.view_contact',
+        weight=5000
+    )
+
+    def get_children(self, request, parent):
+        return Contact.objects.annotate(
+            assignment_count=count_related(ContactAssignment, 'contact')
+        ).restrict(request.user, 'view').filter(assignments__object_id=parent.pk)
+
+    def get_extra_context(self, request, instance):
+        return {
+            'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
+        }
+
 #
 #
 # Tenant groups
 # Tenant groups
 #
 #
 
 
+
 class TenantGroupListView(generic.ObjectListView):
 class TenantGroupListView(generic.ObjectListView):
     queryset = TenantGroup.objects.add_related_count(
     queryset = TenantGroup.objects.add_related_count(
         TenantGroup.objects.all(),
         TenantGroup.objects.all(),
@@ -165,6 +188,11 @@ class TenantBulkDeleteView(generic.BulkDeleteView):
     table = tables.TenantTable
     table = tables.TenantTable
 
 
 
 
+@register_model_view(Tenant, 'contacts')
+class TenantContactsView(ObjectContactsView):
+    queryset = Tenant.objects.all()
+
+
 #
 #
 # Contact groups
 # Contact groups
 #
 #
@@ -342,11 +370,11 @@ class ContactBulkDeleteView(generic.BulkDeleteView):
     filterset = filtersets.ContactFilterSet
     filterset = filtersets.ContactFilterSet
     table = tables.ContactTable
     table = tables.ContactTable
 
 
-
 #
 #
 # Contact assignments
 # Contact assignments
 #
 #
 
 
+
 class ContactAssignmentListView(generic.ObjectListView):
 class ContactAssignmentListView(generic.ObjectListView):
     queryset = ContactAssignment.objects.all()
     queryset = ContactAssignment.objects.all()
     filterset = filtersets.ContactAssignmentFilterSet
     filterset = filtersets.ContactAssignmentFilterSet

+ 17 - 1
netbox/virtualization/views.py

@@ -9,9 +9,10 @@ from dcim.filtersets import DeviceFilterSet
 from dcim.models import Device
 from dcim.models import Device
 from dcim.tables import DeviceTable
 from dcim.tables import DeviceTable
 from extras.views import ObjectConfigContextView
 from extras.views import ObjectConfigContextView
-from ipam.models import IPAddress, Service
+from ipam.models import IPAddress
 from ipam.tables import InterfaceVLANTable
 from ipam.tables import InterfaceVLANTable
 from netbox.views import generic
 from netbox.views import generic
+from tenancy.views import ObjectContactsView
 from utilities.utils import count_related
 from utilities.utils import count_related
 from utilities.views import ViewTab, register_model_view
 from utilities.views import ViewTab, register_model_view
 from . import filtersets, forms, tables
 from . import filtersets, forms, tables
@@ -140,6 +141,11 @@ class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
     table = tables.ClusterGroupTable
     table = tables.ClusterGroupTable
 
 
 
 
+@register_model_view(ClusterGroup, 'contacts')
+class ClusterGroupContactsView(ObjectContactsView):
+    queryset = ClusterGroup.objects.all()
+
+
 #
 #
 # Clusters
 # Clusters
 #
 #
@@ -312,6 +318,11 @@ class ClusterRemoveDevicesView(generic.ObjectEditView):
         })
         })
 
 
 
 
+@register_model_view(Cluster, 'contacts')
+class ClusterContactsView(ObjectContactsView):
+    queryset = Cluster.objects.all()
+
+
 #
 #
 # Virtual machines
 # Virtual machines
 #
 #
@@ -390,6 +401,11 @@ class VirtualMachineBulkDeleteView(generic.BulkDeleteView):
     table = tables.VirtualMachineTable
     table = tables.VirtualMachineTable
 
 
 
 
+@register_model_view(VirtualMachine, 'contacts')
+class VirtualMachineContactsView(ObjectContactsView):
+    queryset = VirtualMachine.objects.all()
+
+
 #
 #
 # VM interfaces
 # VM interfaces
 #
 #