diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py
index e5f4faee1..f1cfdd1d5 100644
--- a/netbox/circuits/views.py
+++ b/netbox/circuits/views.py
@@ -1,10 +1,10 @@
from django.contrib import messages
from django.db import transaction
-from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect, render
from dcim.views import PathTraceView
from netbox.views import generic
+from tenancy.views import ObjectContactsView
from utilities.forms import ConfirmationForm
from utilities.utils import count_related
from utilities.views import register_model_view
@@ -73,6 +73,11 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
table = tables.ProviderTable
+@register_model_view(Provider, 'contacts')
+class ProviderContactsView(ObjectContactsView):
+ queryset = Provider.objects.all()
+
+
#
# ProviderAccounts
#
@@ -134,6 +139,11 @@ class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
table = tables.ProviderAccountTable
+@register_model_view(ProviderAccount, 'contacts')
+class ProviderAccountContactsView(ObjectContactsView):
+ queryset = ProviderAccount.objects.all()
+
+
#
# Provider networks
#
@@ -389,6 +399,11 @@ class CircuitSwapTerminations(generic.ObjectEditView):
})
+@register_model_view(Circuit, 'contacts')
+class CircuitContactsView(ObjectContactsView):
+ queryset = Circuit.objects.all()
+
+
#
# Circuit terminations
#
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index bcbbf1739..0def4f4a8 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -20,6 +20,7 @@ from extras.views import ObjectConfigContextView
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
from ipam.tables import InterfaceVLANTable
from netbox.views import generic
+from tenancy.views import ObjectContactsView
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.permissions import get_permission_for_model
@@ -267,6 +268,11 @@ class RegionBulkDeleteView(generic.BulkDeleteView):
table = tables.RegionTable
+@register_model_view(Region, 'contacts')
+class RegionContactsView(ObjectContactsView):
+ queryset = Region.objects.all()
+
+
#
# Site groups
#
@@ -342,6 +348,11 @@ class SiteGroupBulkDeleteView(generic.BulkDeleteView):
table = tables.SiteGroupTable
+@register_model_view(SiteGroup, 'contacts')
+class SiteGroupContactsView(ObjectContactsView):
+ queryset = SiteGroup.objects.all()
+
+
#
# Sites
#
@@ -435,6 +446,11 @@ class SiteBulkDeleteView(generic.BulkDeleteView):
table = tables.SiteTable
+@register_model_view(Site, 'contacts')
+class SiteContactsView(ObjectContactsView):
+ queryset = Site.objects.all()
+
+
#
# Locations
#
@@ -523,6 +539,11 @@ class LocationBulkDeleteView(generic.BulkDeleteView):
table = tables.LocationTable
+@register_model_view(Location, 'contacts')
+class LocationContactsView(ObjectContactsView):
+ queryset = Location.objects.all()
+
+
#
# Rack roles
#
@@ -740,6 +761,11 @@ class RackBulkDeleteView(generic.BulkDeleteView):
table = tables.RackTable
+@register_model_view(Rack, 'contacts')
+class RackContactsView(ObjectContactsView):
+ queryset = Rack.objects.all()
+
+
#
# Rack reservations
#
@@ -874,6 +900,11 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView):
table = tables.ManufacturerTable
+@register_model_view(Manufacturer, 'contacts')
+class ManufacturerContactsView(ObjectContactsView):
+ queryset = Manufacturer.objects.all()
+
+
#
# Device types
#
@@ -2088,6 +2119,11 @@ class DeviceBulkRenameView(generic.BulkRenameView):
table = tables.DeviceTable
+@register_model_view(Device, 'contacts')
+class DeviceContactsView(ObjectContactsView):
+ queryset = Device.objects.all()
+
+
#
# Modules
#
@@ -3469,6 +3505,11 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView):
table = tables.PowerPanelTable
+@register_model_view(PowerPanel, 'contacts')
+class PowerPanelContactsView(ObjectContactsView):
+ queryset = PowerPanel.objects.all()
+
+
#
# Power feeds
#
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
index 6b19b502d..6b73a061b 100644
--- a/netbox/ipam/views.py
+++ b/netbox/ipam/views.py
@@ -9,6 +9,7 @@ from circuits.models import Provider
from dcim.filtersets import InterfaceFilterSet
from dcim.models import Interface, Site
from netbox.views import generic
+from tenancy.views import ObjectContactsView
from utilities.utils import count_related
from utilities.views import ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet
@@ -1300,6 +1301,11 @@ class L2VPNBulkDeleteView(generic.BulkDeleteView):
table = tables.L2VPNTable
+@register_model_view(L2VPN, 'contacts')
+class L2VPNContactsView(ObjectContactsView):
+ queryset = L2VPN.objects.all()
+
+
#
# L2VPN terminations
#
diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html
index ee994e959..a5913e2ad 100644
--- a/netbox/templates/circuits/circuit.html
+++ b/netbox/templates/circuits/circuit.html
@@ -70,7 +70,6 @@
{% 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 'inc/panels/contacts.html' %}
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}
diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html
index 5a565ea29..c721d5a58 100644
--- a/netbox/templates/circuits/provider.html
+++ b/netbox/templates/circuits/provider.html
@@ -43,7 +43,6 @@
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %}
diff --git a/netbox/templates/circuits/provideraccount.html b/netbox/templates/circuits/provideraccount.html
index 63344ada1..c55663b4a 100644
--- a/netbox/templates/circuits/provideraccount.html
+++ b/netbox/templates/circuits/provideraccount.html
@@ -38,7 +38,6 @@
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/comments.html' %}
{% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %}
diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html
index aa1b80cf7..b0e67269c 100644
--- a/netbox/templates/dcim/device.html
+++ b/netbox/templates/dcim/device.html
@@ -298,7 +298,6 @@
{% endif %}
- {% include 'inc/panels/contacts.html' %}
{% include 'inc/panels/image_attachments.html' %}
diff --git a/netbox/templates/dcim/location.html b/netbox/templates/dcim/location.html
index 193d93f9a..795aeb35f 100644
--- a/netbox/templates/dcim/location.html
+++ b/netbox/templates/dcim/location.html
@@ -65,7 +65,6 @@
{% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/contacts.html' %}
{% include 'dcim/inc/nonracked_devices.html' %}
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}
diff --git a/netbox/templates/dcim/manufacturer.html b/netbox/templates/dcim/manufacturer.html
index a60b3503c..8233b6fc8 100644
--- a/netbox/templates/dcim/manufacturer.html
+++ b/netbox/templates/dcim/manufacturer.html
@@ -51,7 +51,6 @@
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %}
diff --git a/netbox/templates/dcim/powerpanel.html b/netbox/templates/dcim/powerpanel.html
index af08f3023..ea9210ba7 100644
--- a/netbox/templates/dcim/powerpanel.html
+++ b/netbox/templates/dcim/powerpanel.html
@@ -40,7 +40,6 @@
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/contacts.html' %}
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}
diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html
index 9cb046b4e..52b5d4bfe 100644
--- a/netbox/templates/dcim/rack.html
+++ b/netbox/templates/dcim/rack.html
@@ -191,7 +191,6 @@
{% include 'inc/panels/related_objects.html' %}
{% include 'dcim/inc/nonracked_devices.html' %}
- {% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %}
diff --git a/netbox/templates/dcim/region.html b/netbox/templates/dcim/region.html
index 85587e4b5..05cc424d7 100644
--- a/netbox/templates/dcim/region.html
+++ b/netbox/templates/dcim/region.html
@@ -46,7 +46,6 @@
{% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %}
diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html
index 91fdba7be..697737ceb 100644
--- a/netbox/templates/dcim/site.html
+++ b/netbox/templates/dcim/site.html
@@ -131,7 +131,6 @@
{% include 'inc/panels/related_objects.html' with filter_name='site_id' %}
- {% include 'inc/panels/contacts.html' %}
diff --git a/netbox/templates/dcim/sitegroup.html b/netbox/templates/dcim/sitegroup.html
index 2cf8e7168..819022a34 100644
--- a/netbox/templates/dcim/sitegroup.html
+++ b/netbox/templates/dcim/sitegroup.html
@@ -42,7 +42,6 @@
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/contacts.html' %}
{% plugin_left_page object %}
diff --git a/netbox/templates/inc/panels/contacts.html b/netbox/templates/inc/panels/contacts.html
deleted file mode 100644
index 359ad8d7e..000000000
--- a/netbox/templates/inc/panels/contacts.html
+++ /dev/null
@@ -1,63 +0,0 @@
-{% load helpers %}
-
-
-
-
- {% with contacts=object.contacts.all %}
- {% if contacts.exists %}
-
-
- Name |
- Role |
- Priority |
- Phone |
- Email |
- |
-
- {% for contact in contacts %}
-
- {{ contact.contact|linkify }} |
- {{ contact.role|placeholder }} |
- {{ contact.get_priority_display|placeholder }} |
-
- {% if contact.contact.phone %}
- {{ contact.contact.phone }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
- {% if contact.contact.email %}
- {{ contact.contact.email }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
- {% if perms.tenancy.change_contactassignment %}
-
-
-
- {% endif %}
- {% if perms.tenancy.delete_contactassignment %}
-
-
-
- {% endif %}
- |
-
- {% endfor %}
-
- {% else %}
-
None
- {% endif %}
- {% endwith %}
-
- {% if perms.tenancy.add_contactassignment %}
-
- {% endif %}
-
diff --git a/netbox/templates/ipam/l2vpn.html b/netbox/templates/ipam/l2vpn.html
index 87050eb26..8896dd6c2 100644
--- a/netbox/templates/ipam/l2vpn.html
+++ b/netbox/templates/ipam/l2vpn.html
@@ -37,7 +37,6 @@
{% plugin_left_page object %}
- {% include 'inc/panels/contacts.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
diff --git a/netbox/templates/tenancy/object_contacts.html b/netbox/templates/tenancy/object_contacts.html
new file mode 100644
index 000000000..aca63a379
--- /dev/null
+++ b/netbox/templates/tenancy/object_contacts.html
@@ -0,0 +1,27 @@
+{% extends base_template %}
+{% load helpers %}
+
+{% block extra_controls %}
+ {% if perms.tenancy.add_contactassignment %}
+
+ Add a contact
+
+ {% endif %}
+{% endblock %}
+
+{% block content %}
+ {% include 'inc/table_controls_htmx.html' with table_modal="ContactTable_config" %}
+
+{% endblock content %}
+
+{% block modals %}
+ {{ block.super }}
+ {% table_config_form table %}
+{% endblock modals %}
diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html
index da48f1ef5..34abe5c01 100644
--- a/netbox/templates/tenancy/tenant.html
+++ b/netbox/templates/tenancy/tenant.html
@@ -30,7 +30,6 @@
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
- {% include 'inc/panels/contacts.html' %}
{% plugin_left_page object %}
diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html
index 3dfef108b..508bca547 100644
--- a/netbox/templates/virtualization/cluster.html
+++ b/netbox/templates/virtualization/cluster.html
@@ -84,7 +84,6 @@
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %}
diff --git a/netbox/templates/virtualization/clustergroup.html b/netbox/templates/virtualization/clustergroup.html
index 510433068..2496ad085 100644
--- a/netbox/templates/virtualization/clustergroup.html
+++ b/netbox/templates/virtualization/clustergroup.html
@@ -37,7 +37,6 @@
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %}
diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html
index 5098a2f8f..51fd8aa80 100644
--- a/netbox/templates/virtualization/virtualmachine.html
+++ b/netbox/templates/virtualization/virtualmachine.html
@@ -158,7 +158,6 @@
{% endif %}
- {% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %}
diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py
index b9ada8640..5f8a7e314 100644
--- a/netbox/tenancy/views.py
+++ b/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 netbox.views import generic
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 wireless.models import WirelessLAN, WirelessLink
from . import filtersets, forms, tables
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
#
+
class TenantGroupListView(generic.ObjectListView):
queryset = TenantGroup.objects.add_related_count(
TenantGroup.objects.all(),
@@ -165,6 +188,11 @@ class TenantBulkDeleteView(generic.BulkDeleteView):
table = tables.TenantTable
+@register_model_view(Tenant, 'contacts')
+class TenantContactsView(ObjectContactsView):
+ queryset = Tenant.objects.all()
+
+
#
# Contact groups
#
@@ -342,11 +370,11 @@ class ContactBulkDeleteView(generic.BulkDeleteView):
filterset = filtersets.ContactFilterSet
table = tables.ContactTable
-
#
# Contact assignments
#
+
class ContactAssignmentListView(generic.ObjectListView):
queryset = ContactAssignment.objects.all()
filterset = filtersets.ContactAssignmentFilterSet
diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py
index 9014aa9dd..4a501e14e 100644
--- a/netbox/virtualization/views.py
+++ b/netbox/virtualization/views.py
@@ -9,9 +9,10 @@ from dcim.filtersets import DeviceFilterSet
from dcim.models import Device
from dcim.tables import DeviceTable
from extras.views import ObjectConfigContextView
-from ipam.models import IPAddress, Service
+from ipam.models import IPAddress
from ipam.tables import InterfaceVLANTable
from netbox.views import generic
+from tenancy.views import ObjectContactsView
from utilities.utils import count_related
from utilities.views import ViewTab, register_model_view
from . import filtersets, forms, tables
@@ -140,6 +141,11 @@ class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
table = tables.ClusterGroupTable
+@register_model_view(ClusterGroup, 'contacts')
+class ClusterGroupContactsView(ObjectContactsView):
+ queryset = ClusterGroup.objects.all()
+
+
#
# Clusters
#
@@ -312,6 +318,11 @@ class ClusterRemoveDevicesView(generic.ObjectEditView):
})
+@register_model_view(Cluster, 'contacts')
+class ClusterContactsView(ObjectContactsView):
+ queryset = Cluster.objects.all()
+
+
#
# Virtual machines
#
@@ -390,6 +401,11 @@ class VirtualMachineBulkDeleteView(generic.BulkDeleteView):
table = tables.VirtualMachineTable
+@register_model_view(VirtualMachine, 'contacts')
+class VirtualMachineContactsView(ObjectContactsView):
+ queryset = VirtualMachine.objects.all()
+
+
#
# VM interfaces
#