VRF
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 d6de8f3cb..697737ceb 100644
--- a/netbox/templates/dcim/site.html
+++ b/netbox/templates/dcim/site.html
@@ -87,11 +87,13 @@
Physical Address
{% if object.physical_address %}
-
+ {% if config.MAPS_URL %}
+
+ {% endif %}
{{ object.physical_address|linebreaksbr }}
{% else %}
{{ ''|placeholder }}
@@ -106,11 +108,13 @@
GPS Coordinates
{% if object.latitude and object.longitude %}
-
+ {% if config.MAPS_URL %}
+
+ {% endif %}
{{ object.latitude }}, {{ object.longitude }}
{% else %}
{{ ''|placeholder }}
@@ -127,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/inc/panels/custom_fields.html b/netbox/templates/inc/panels/custom_fields.html
index 45843eea5..7f5f4cc27 100644
--- a/netbox/templates/inc/panels/custom_fields.html
+++ b/netbox/templates/inc/panels/custom_fields.html
@@ -12,8 +12,15 @@
{% for field, value in fields.items %}
-
- {{ field }}
+ {{ field }}
+ {% if field.description %}
+
+ {% endif %}
{% customfield_value field value %}
diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html
index c649f1dad..e58ac736f 100644
--- a/netbox/templates/ipam/ipaddress.html
+++ b/netbox/templates/ipam/ipaddress.html
@@ -3,13 +3,6 @@
{% load plugins %}
{% load render_table from django_tables2 %}
-{% block breadcrumbs %}
- {{ block.super }}
- {% if object.vrf %}
- {{ object.vrf }}
- {% endif %}
-{% endblock %}
-
{% block content %}
@@ -116,7 +109,6 @@
{% if duplicate_ips_table.rows %}
{% include 'inc/panel_table.html' with table=duplicate_ips_table heading='Duplicate IPs' panel_class='danger' %}
{% endif %}
- {% include 'inc/panel_table.html' with table=related_ips_table heading='Related IPs' %}
{{ object.vrf }}
+ {% endif %}
+{% endblock %}
diff --git a/netbox/templates/ipam/ipaddress/ip_addresses.html b/netbox/templates/ipam/ipaddress/ip_addresses.html
new file mode 100644
index 000000000..7034329aa
--- /dev/null
+++ b/netbox/templates/ipam/ipaddress/ip_addresses.html
@@ -0,0 +1,19 @@
+{% extends 'ipam/ipaddress/base.html' %}
+{% load helpers %}
+
+{% block content %}
+ {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
+
+{% endblock content %}
+
+{% block modals %}
+ {{ block.super }}
+ {% table_config_form table %}
+{% endblock modals %}
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/templates/virtualization/vminterface.html b/netbox/templates/virtualization/vminterface.html
index a7d4d92ba..82071bdaa 100644
--- a/netbox/templates/virtualization/vminterface.html
+++ b/netbox/templates/virtualization/vminterface.html
@@ -59,7 +59,7 @@
MAC Address
- {{ object.mac_address|placeholder }}
+ {{ object.mac_address|placeholder }}
802.1Q Mode
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/utilities/forms/mixins.py b/netbox/utilities/forms/mixins.py
index dc9c3eb80..7cdb9731e 100644
--- a/netbox/utilities/forms/mixins.py
+++ b/netbox/utilities/forms/mixins.py
@@ -32,11 +32,11 @@ class BootstrapMixin:
elif isinstance(field.widget, forms.CheckboxInput):
field.widget.attrs['class'] = f'{css} form-check-input'
- elif isinstance(field.widget, forms.SelectMultiple):
- if 'size' not in field.widget.attrs:
- field.widget.attrs['class'] = f'{css} netbox-static-select'
+ elif isinstance(field.widget, forms.SelectMultiple) and 'size' in field.widget.attrs:
+ # Use native Bootstrap class for multi-line widgets
+ field.widget.attrs['class'] = f'{css} form-select form-select-sm'
- elif isinstance(field.widget, forms.Select):
+ elif isinstance(field.widget, (forms.Select, forms.SelectMultiple)):
field.widget.attrs['class'] = f'{css} netbox-static-select'
else:
diff --git a/netbox/utilities/rqworker.py b/netbox/utilities/rqworker.py
index 5866dfee0..61f594767 100644
--- a/netbox/utilities/rqworker.py
+++ b/netbox/utilities/rqworker.py
@@ -1,11 +1,12 @@
from django_rq.queues import get_connection
-from rq import Worker
+from rq import Retry, Worker
from netbox.config import get_config
from netbox.constants import RQ_QUEUE_DEFAULT
__all__ = (
'get_queue_for_model',
+ 'get_rq_retry',
'get_workers_for_queue',
)
@@ -22,3 +23,14 @@ def get_workers_for_queue(queue_name):
Returns True if a worker process is currently servicing the specified queue.
"""
return Worker.count(get_connection(queue_name))
+
+
+def get_rq_retry():
+ """
+ If RQ_RETRY_MAX is defined and greater than zero, instantiate and return a Retry object to be
+ used when queuing a job. Otherwise, return None.
+ """
+ retry_max = get_config().RQ_RETRY_MAX
+ retry_interval = get_config().RQ_RETRY_INTERVAL
+ if retry_max:
+ return Retry(max=retry_max, interval=retry_interval)
diff --git a/netbox/virtualization/forms/bulk_import.py b/netbox/virtualization/forms/bulk_import.py
index a229bd935..15651f2ae 100644
--- a/netbox/virtualization/forms/bulk_import.py
+++ b/netbox/virtualization/forms/bulk_import.py
@@ -65,7 +65,7 @@ class ClusterImportForm(NetBoxModelImportForm):
class Meta:
model = Cluster
- fields = ('name', 'type', 'group', 'status', 'site', 'description', 'comments', 'tags')
+ fields = ('name', 'type', 'group', 'status', 'site', 'tenant', 'description', 'comments', 'tags')
class VirtualMachineImportForm(NetBoxModelImportForm):
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
#
diff --git a/requirements.txt b/requirements.txt
index c3d9c8c38..6757f3260 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,8 @@
bleach==6.0.0
-boto3==1.26.127
+boto3==1.26.138
Django==4.1.9
-django-cors-headers==3.14.0
-django-debug-toolbar==4.0.0
+django-cors-headers==4.0.0
+django-debug-toolbar==4.1.0
django-filter==23.2
django-graphiql-debug-toolbar==0.2.0
django-mptt==0.14
@@ -10,7 +10,7 @@ django-pglocks==1.0.4
django-prometheus==2.3.1
django-redis==5.2.0
django-rich==1.5.0
-django-rq==2.8.0
+django-rq==2.8.1
django-tables2==2.5.3
django-taggit==4.0.0
django-timezone-field==5.0
@@ -19,17 +19,17 @@ drf-spectacular==0.26.2
drf-spectacular-sidecar==2023.5.1
dulwich==0.21.5
feedparser==6.0.10
-graphene-django==3.0.0
+graphene-django==3.0.2
gunicorn==20.1.0
Jinja2==3.1.2
Markdown==3.3.7
-mkdocs-material==9.1.9
+mkdocs-material==9.1.14
mkdocstrings[python-legacy]==0.21.2
netaddr==0.8.0
Pillow==9.5.0
psycopg2-binary==2.9.6
PyYAML==6.0
-sentry-sdk==1.22.1
+sentry-sdk==1.23.1
social-auth-app-django==5.2.0
social-auth-core[openidconnect]==4.4.2
svgwrite==1.4.3