From 63ba9fb38cde90ec9183d1cb234a7de12450e4d2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 5 Jul 2023 11:39:35 -0400 Subject: [PATCH 01/15] Fixes #11335: Default manager for ObjectChange should filter by installed apps (#11709) * Fixes #11335: Default manager for ObjectChange should filter by installed apps * Employ canonical model discovery mechanism * Move filtering logic to valid_models() queryset method * fixed import to avoid content type does not exist * Cleanup --------- Co-authored-by: Abhimanyu Saharan --- netbox/extras/api/views.py | 2 +- netbox/extras/models/change_logging.py | 4 ++-- netbox/extras/querysets.py | 13 +++++++++++++ netbox/extras/tests/test_api.py | 3 ++- netbox/extras/views.py | 8 ++++---- netbox/users/views.py | 4 +++- 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 3f796d7f8..f4b5a1433 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -368,7 +368,7 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet): Retrieve a list of recent changes. """ metadata_class = ContentTypeMetadata - queryset = ObjectChange.objects.prefetch_related('user') + queryset = ObjectChange.objects.valid_models().prefetch_related('user') serializer_class = serializers.ObjectChangeSerializer filterset_class = filtersets.ObjectChangeFilterSet diff --git a/netbox/extras/models/change_logging.py b/netbox/extras/models/change_logging.py index e2b118b84..2cb53ed01 100644 --- a/netbox/extras/models/change_logging.py +++ b/netbox/extras/models/change_logging.py @@ -5,7 +5,7 @@ from django.db import models from django.urls import reverse from extras.choices import * -from utilities.querysets import RestrictedQuerySet +from ..querysets import ObjectChangeQuerySet __all__ = ( 'ObjectChange', @@ -82,7 +82,7 @@ class ObjectChange(models.Model): null=True ) - objects = RestrictedQuerySet.as_manager() + objects = ObjectChangeQuerySet.as_manager() class Meta: ordering = ['-time'] diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py index 2b97af0fb..2e6f93b93 100644 --- a/netbox/extras/querysets.py +++ b/netbox/extras/querysets.py @@ -1,3 +1,5 @@ +from django.apps import apps +from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.aggregates import JSONBAgg from django.db.models import OuterRef, Subquery, Q @@ -151,3 +153,14 @@ class ConfigContextModelQuerySet(RestrictedQuerySet): ) return base_query + + +class ObjectChangeQuerySet(RestrictedQuerySet): + + def valid_models(self): + # Exclude any change records which refer to an instance of a model that's no longer installed. This + # can happen when a plugin is removed but its data remains in the database, for example. + content_type_ids = set( + ct.pk for ct in ContentType.objects.get_for_models(*apps.get_models()).values() + ) + return self.filter(changed_object_type_id__in=content_type_ids) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index b59481a36..086c8e246 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -8,7 +8,6 @@ from rest_framework import status from core.choices import ManagedFileRootPathChoices from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site -from extras.api.views import ReportViewSet, ScriptViewSet from extras.models import * from extras.reports import Report from extras.scripts import BooleanVar, IntegerVar, Script, StringVar @@ -579,6 +578,7 @@ class ReportTest(APITestCase): super().setUp() # Monkey-patch the API viewset's _get_report() method to return our test Report above + from extras.api.views import ReportViewSet ReportViewSet._get_report = self.get_test_report def test_get_report(self): @@ -621,6 +621,7 @@ class ScriptTest(APITestCase): super().setUp() # Monkey-patch the API viewset's _get_script() method to return our test Script above + from extras.api.views import ScriptViewSet ScriptViewSet._get_script = self.get_test_script def test_get_script(self): diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 6cbadf09d..6ba63ab58 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -511,7 +511,7 @@ class ConfigTemplateBulkSyncDataView(generic.BulkSyncDataView): # class ObjectChangeListView(generic.ObjectListView): - queryset = ObjectChange.objects.all() + queryset = ObjectChange.objects.valid_models() filterset = filtersets.ObjectChangeFilterSet filterset_form = forms.ObjectChangeFilterForm table = tables.ObjectChangeTable @@ -521,10 +521,10 @@ class ObjectChangeListView(generic.ObjectListView): @register_model_view(ObjectChange) class ObjectChangeView(generic.ObjectView): - queryset = ObjectChange.objects.all() + queryset = ObjectChange.objects.valid_models() def get_extra_context(self, request, instance): - related_changes = ObjectChange.objects.restrict(request.user, 'view').filter( + related_changes = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter( request_id=instance.request_id ).exclude( pk=instance.pk @@ -534,7 +534,7 @@ class ObjectChangeView(generic.ObjectView): orderable=False ) - objectchanges = ObjectChange.objects.restrict(request.user, 'view').filter( + objectchanges = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter( changed_object_type=instance.changed_object_type, changed_object_id=instance.changed_object_id, ) diff --git a/netbox/users/views.py b/netbox/users/views.py index a82620914..05648e2e3 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -159,7 +159,9 @@ class ProfileView(LoginRequiredMixin, View): def get(self, request): # Compile changelog table - changelog = ObjectChange.objects.restrict(request.user, 'view').filter(user=request.user).prefetch_related( + changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter( + user=request.user + ).prefetch_related( 'changed_object_type' )[:20] changelog_table = ObjectChangeTable(changelog) From 07ae7c8a6e04ecc6e641044330fd4d03e4997252 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 5 Jul 2023 11:43:53 -0400 Subject: [PATCH 02/15] Changelog for #11335, #12760, #12842, #12951, #12955 --- docs/release-notes/version-3.5.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 7b82ed07c..beb0a1991 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -5,11 +5,16 @@ ### Enhancements * [#12945](https://github.com/netbox-community/netbox/issues/12945) - Add 100GE QSFP-DD interface type +* [#12955](https://github.com/netbox-community/netbox/issues/12955) - Include additional contact details on contact assignments table ### Bug Fixes +* [#11335](https://github.com/netbox-community/netbox/issues/11335) - Exclude stale content types when retrieving changelog records * [#12533](https://github.com/netbox-community/netbox/issues/12533) - Fix REST API validation of null values for several interface attributes +* [#12760](https://github.com/netbox-community/netbox/issues/12760) - Avoid rendering partial HTMX responses when restoring browser tabs +* [#12842](https://github.com/netbox-community/netbox/issues/12842) - Improve handling of exceptions when loading reports * [#12849](https://github.com/netbox-community/netbox/issues/12849) - Fix LDAP group permissions assignment for API clients +* [#12951](https://github.com/netbox-community/netbox/issues/12951) - Display consistent parent information for each termination under cable view * [#12953](https://github.com/netbox-community/netbox/issues/12953) - Fix designation of primary IP addresses during interface assignment * [#12960](https://github.com/netbox-community/netbox/issues/12960) - Fix OpenAPI schema for various choice fields * [#12961](https://github.com/netbox-community/netbox/issues/12961) - Set correct return URL for object contacts tabs From 4355ee6407946a0f858ec8bba1dc115c4f504d7e Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 6 Jul 2023 12:45:11 +0700 Subject: [PATCH 03/15] 12092 allow setnull for bulk edit power port maximum and allocated draw --- netbox/dcim/forms/bulk_edit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index bc9693afb..f1abdef1d 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -1106,7 +1106,7 @@ class PowerPortBulkEditForm( (None, ('module', 'type', 'label', 'description', 'mark_connected')), ('Power', ('maximum_draw', 'allocated_draw')), ) - nullable_fields = ('module', 'label', 'description') + nullable_fields = ('module', 'label', 'description', 'maximum_draw', 'allocated_draw') class PowerOutletBulkEditForm( From 5f0922713fa03135fb837c9ff2cb31db2691c461 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 29 Jun 2023 15:35:53 -0400 Subject: [PATCH 04/15] Fixes #13047: Add annotate_asn_count() to ASNRange manager --- netbox/ipam/models/asns.py | 3 +++ netbox/ipam/querysets.py | 26 +++++++++++++++++++++++++- netbox/ipam/tables/asn.py | 9 ++++----- netbox/ipam/views.py | 10 +++------- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/netbox/ipam/models/asns.py b/netbox/ipam/models/asns.py index a07cbb789..6c0b5231b 100644 --- a/netbox/ipam/models/asns.py +++ b/netbox/ipam/models/asns.py @@ -4,6 +4,7 @@ from django.urls import reverse from django.utils.translation import gettext as _ from ipam.fields import ASNField +from ipam.querysets import ASNRangeQuerySet from netbox.models import OrganizationalModel, PrimaryModel __all__ = ( @@ -37,6 +38,8 @@ class ASNRange(OrganizationalModel): null=True ) + objects = ASNRangeQuerySet.as_manager() + class Meta: ordering = ('name',) verbose_name = 'ASN range' diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py index 9f4463f61..d6b23b843 100644 --- a/netbox/ipam/querysets.py +++ b/netbox/ipam/querysets.py @@ -1,9 +1,33 @@ from django.contrib.contenttypes.models import ContentType -from django.db.models import Q +from django.db.models import Count, OuterRef, Q, Subquery, Value from django.db.models.expressions import RawSQL from utilities.querysets import RestrictedQuerySet +__all__ = ( + 'ASNRangeQuerySet', + 'PrefixQuerySet', + 'VLANQuerySet', +) + + +class ASNRangeQuerySet(RestrictedQuerySet): + + def annotate_asn_counts(self): + """ + Annotate the number of ASNs which appear within each range. + """ + from .models import ASN + + # Because ASN does not have a foreign key to ASNRange, we create a fake column "_" with a consistent value + # that we can use to count ASNs and return a single value per ASNRange. + asns = ASN.objects.filter( + asn__gte=OuterRef('start'), + asn__lte=OuterRef('end') + ).order_by().annotate(_=Value(1)).values('_').annotate(c=Count('*')).values('c') + + return self.annotate(asn_count=Subquery(asns)) + class PrefixQuerySet(RestrictedQuerySet): diff --git a/netbox/ipam/tables/asn.py b/netbox/ipam/tables/asn.py index 511e914ec..356f2fc17 100644 --- a/netbox/ipam/tables/asn.py +++ b/netbox/ipam/tables/asn.py @@ -21,10 +21,8 @@ class ASNRangeTable(TenancyColumnsMixin, NetBoxTable): tags = columns.TagColumn( url_name='ipam:asnrange_list' ) - asn_count = columns.LinkedCountColumn( - viewname='ipam:asn_list', - url_params={'asn_id': 'pk'}, - verbose_name=_('ASN Count') + asn_count = tables.Column( + verbose_name=_('ASNs') ) class Meta(NetBoxTable.Meta): @@ -59,7 +57,8 @@ class ASNTable(TenancyColumnsMixin, NetBoxTable): verbose_name=_('Provider Count') ) sites = columns.ManyToManyColumn( - linkify_item=True + linkify_item=True, + verbose_name=_('Sites') ) comments = columns.MarkdownColumn() tags = columns.TagColumn( diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 6b73a061b..6efaef8ea 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -198,7 +198,7 @@ class RIRBulkDeleteView(generic.BulkDeleteView): # class ASNRangeListView(generic.ObjectListView): - queryset = ASNRange.objects.all() + queryset = ASNRange.objects.annotate_asn_counts() filterset = filtersets.ASNRangeFilterSet filterset_form = forms.ASNRangeFilterForm table = tables.ASNRangeTable @@ -247,18 +247,14 @@ class ASNRangeBulkImportView(generic.BulkImportView): class ASNRangeBulkEditView(generic.BulkEditView): - queryset = ASNRange.objects.annotate( - site_count=count_related(Site, 'asns') - ) + queryset = ASNRange.objects.annotate_asn_counts() filterset = filtersets.ASNRangeFilterSet table = tables.ASNRangeTable form = forms.ASNRangeBulkEditForm class ASNRangeBulkDeleteView(generic.BulkDeleteView): - queryset = ASNRange.objects.annotate( - site_count=count_related(Site, 'asns') - ) + queryset = ASNRange.objects.annotate_asn_counts() filterset = filtersets.ASNRangeFilterSet table = tables.ASNRangeTable From 860be780ad3b3887bda9e7606e869cdccba89b0d Mon Sep 17 00:00:00 2001 From: Anthony Brissonnet <137606620+netopsab@users.noreply.github.com> Date: Thu, 6 Jul 2023 15:28:45 +0200 Subject: [PATCH 05/15] Fix #12579 create cable and add another error (#13007) * fix create cable and add another error #12579 * fix return proper parent object field * improve code and wokflow --------- Co-authored-by: netopsab --- netbox/dcim/views.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index b52e0afa5..008db382a 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -3131,6 +3131,19 @@ class CableEditView(generic.ObjectEditView): return obj + def get_extra_addanother_params(self, request): + + params = { + 'a_terminations_type': request.GET.get('a_terminations_type'), + 'b_terminations_type': request.GET.get('b_terminations_type') + } + + for key in request.POST: + if 'device' in key or 'power_panel' in key or 'circuit' in key: + params.update({key: request.POST.get(key)}) + + return params + @register_model_view(Cable, 'delete') class CableDeleteView(generic.ObjectDeleteView): From 16ee42ac388466f7829b9fc70888554c5d1dc655 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sat, 24 Jun 2023 00:29:37 +0530 Subject: [PATCH 06/15] fixes prechange snapshot #12617 --- netbox/ipam/forms/model_forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 6fa0f95ea..a3c218fc9 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -379,6 +379,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm): interface = self.instance.assigned_object if type(interface) in (Interface, VMInterface): parent = interface.parent_object + parent.snapshot() if self.cleaned_data['primary_for_parent']: if ipaddress.address.version == 4: parent.primary_ip4 = ipaddress From ffe4558ec5c94f7271fd348ec29e67e24d827065 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 6 Jul 2023 18:41:42 +0530 Subject: [PATCH 07/15] fixes search for vdc #13100 --- netbox/dcim/filtersets.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index e784be8e8..5ddaf9a9a 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1077,10 +1077,13 @@ class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet): def search(self, queryset, name, value): if not value.strip(): return queryset - return queryset.filter( - Q(name__icontains=value) | - Q(identifier=value.strip()) - ).distinct() + + qs_filter = Q(name__icontains=value) + try: + qs_filter |= Q(identifier=int(value)) + except ValueError: + pass + return queryset.filter(qs_filter).distinct() def _has_primary_ip(self, queryset, name, value): params = Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False) From 8143c6e03b6455e4d36c8f6da5d85d764c5d1eb3 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 6 Jul 2023 18:51:51 +0530 Subject: [PATCH 08/15] adds object change for contact assignment #13065 --- netbox/tenancy/models/contacts.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/netbox/tenancy/models/contacts.py b/netbox/tenancy/models/contacts.py index 440541b5f..1df5e3305 100644 --- a/netbox/tenancy/models/contacts.py +++ b/netbox/tenancy/models/contacts.py @@ -136,3 +136,8 @@ class ContactAssignment(ChangeLoggedModel): def get_absolute_url(self): return reverse('tenancy:contact', args=[self.contact.pk]) + + def to_objectchange(self, action): + objectchange = super().to_objectchange(action) + objectchange.related_object = self.object + return objectchange From 62bdb90f61c2b83148b3c0008d64e5b2168db036 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 6 Jul 2023 23:49:55 +0530 Subject: [PATCH 09/15] Adds copy content button (#12584) * adds copy content button #12499 * adds newline * Omit hash mark from target string * Clean up HTML element IDs --------- Co-authored-by: Jeremy Stretch --- netbox/ipam/tables/ip.py | 31 ++++++++++++++++-- netbox/project-static/dist/netbox.js | Bin 530632 -> 530613 bytes netbox/project-static/dist/netbox.js.map | Bin 450874 -> 450868 bytes netbox/project-static/src/clipboard.ts | 2 +- netbox/templates/core/datafile.html | 8 ++--- netbox/templates/dcim/device.html | 6 ++-- .../templates/dcim/virtualdevicecontext.html | 14 ++++++-- netbox/templates/users/api_token.html | 6 ++-- .../virtualization/virtualmachine.html | 6 ++-- netbox/users/tables.py | 4 +-- .../templates/builtins/copy_content.html | 3 ++ .../utilities/templatetags/builtins/tags.py | 12 +++++++ 12 files changed, 69 insertions(+), 23 deletions(-) create mode 100644 netbox/utilities/templates/builtins/copy_content.html diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 86d1a3775..aff090f3a 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -19,14 +19,22 @@ __all__ = ( AVAILABLE_LABEL = mark_safe('Available') +AGGREGATE_COPY_BUTTON = """ +{% copy_content record.pk prefix="aggregate_" %} +""" + PREFIX_LINK = """ {% if record.pk %} - {{ record.prefix }} + {{ record.prefix }} {% else %} {{ record.prefix }} {% endif %} """ +PREFIX_COPY_BUTTON = """ +{% copy_content record.pk prefix="prefix_" %} +""" + PREFIX_LINK_WITH_DEPTH = """ {% load helpers %} {% if record.depth %} @@ -40,7 +48,7 @@ PREFIX_LINK_WITH_DEPTH = """ IPADDRESS_LINK = """ {% if record.pk %} - {{ record.address }} + {{ record.address }} {% elif perms.ipam.add_ipaddress %} {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available {% else %} @@ -48,6 +56,10 @@ IPADDRESS_LINK = """ {% endif %} """ +IPADDRESS_COPY_BUTTON = """ +{% copy_content record.pk prefix="ipaddress_" %} +""" + IPADDRESS_ASSIGN_LINK = """ {{ record }} """ @@ -99,7 +111,11 @@ class RIRTable(NetBoxTable): class AggregateTable(TenancyColumnsMixin, NetBoxTable): prefix = tables.Column( linkify=True, - verbose_name='Aggregate' + verbose_name='Aggregate', + attrs={ + # Allow the aggregate to be copied to the clipboard + 'a': {'id': lambda record: f"aggregate_{record.pk}"} + } ) date_added = tables.DateColumn( format="Y-m-d", @@ -116,6 +132,9 @@ class AggregateTable(TenancyColumnsMixin, NetBoxTable): tags = columns.TagColumn( url_name='ipam:aggregate_list' ) + actions = columns.ActionsColumn( + extra_buttons=AGGREGATE_COPY_BUTTON + ) class Meta(NetBoxTable.Meta): model = Aggregate @@ -242,6 +261,9 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable): tags = columns.TagColumn( url_name='ipam:prefix_list' ) + actions = columns.ActionsColumn( + extra_buttons=PREFIX_COPY_BUTTON + ) class Meta(NetBoxTable.Meta): model = Prefix @@ -348,6 +370,9 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable): tags = columns.TagColumn( url_name='ipam:ipaddress_list' ) + actions = columns.ActionsColumn( + extra_buttons=IPADDRESS_COPY_BUTTON + ) class Meta(NetBoxTable.Meta): model = IPAddress diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 9642d15851c5238db66f5a46ac81880ce2f10e1f..b62436d757a2b299e5bf0f33dcc4fbd67ec21e8c 100644 GIT binary patch delta 40 wcmX@HQDN&wg@zW!7N!>F7M2#)7Pc+yaw2TW`FSO&dF|#R?Ay&nI3}_H03@djzW@LL delta 59 zcmdnGQQ^c!g@zW!7N!>F7M2#)7Pc+yaw3u?`Pr#?N;*nOr6nc#d3wqD1(mwRsmVpD P?J6Sd+f_t3Cb9qkYoQZ* diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index f86d50148d419322456611b15cb949883cc3d9c2..ed3833f982a5d0f0d81c6bcf7e90a86aeb99d789 100644 GIT binary patch delta 34 qcmdmWNP5d5>4p}@7N!>F7M2#)Eo^FBY}rnZ&Kb_zjkwr2t^)wp@(S<( delta 40 wcmdmTNP5>H>4p}@7N!>F7M2#)Eo^FBJgJV3=}tPuPL9qQF57ju*f_2O03L1(wEzGB diff --git a/netbox/project-static/src/clipboard.ts b/netbox/project-static/src/clipboard.ts index a04acba39..46ca5e36c 100644 --- a/netbox/project-static/src/clipboard.ts +++ b/netbox/project-static/src/clipboard.ts @@ -2,7 +2,7 @@ import Clipboard from 'clipboard'; import { getElements } from './util'; export function initClipboard(): void { - for (const element of getElements('a.copy-token', 'button.copy-secret')) { + for (const element of getElements('a.copy-content')) { new Clipboard(element); } } diff --git a/netbox/templates/core/datafile.html b/netbox/templates/core/datafile.html index 3d79d17e2..785617ae5 100644 --- a/netbox/templates/core/datafile.html +++ b/netbox/templates/core/datafile.html @@ -39,9 +39,7 @@ Path {{ object.path }} - - - + {% copy_content "datafile_path" %} @@ -56,9 +54,7 @@ SHA256 Hash {{ object.hash }} - - - + {% copy_content "datafile_hash" %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index b0e67269c..df5209add 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -194,12 +194,13 @@ Primary IPv4 {% if object.primary_ip4 %} - {{ object.primary_ip4.address.ip }} + {{ object.primary_ip4.address.ip }} {% if object.primary_ip4.nat_inside %} (NAT for {{ object.primary_ip4.nat_inside.address.ip }}) {% elif object.primary_ip4.nat_outside.exists %} (NAT: {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} + {% copy_content "primary_ip4" %} {% else %} {{ ''|placeholder }} {% endif %} @@ -209,12 +210,13 @@ Primary IPv6 {% if object.primary_ip6 %} - {{ object.primary_ip6.address.ip }} + {{ object.primary_ip6.address.ip }} {% if object.primary_ip6.nat_inside %} (NAT for {{ object.primary_ip6.nat_inside.address.ip }}) {% elif object.primary_ip6.nat_outside.exists %} (NAT: {% for nat in object.primary_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} + {% copy_content "primary_ip6" %} {% else %} {{ ''|placeholder }} {% endif %} diff --git a/netbox/templates/dcim/virtualdevicecontext.html b/netbox/templates/dcim/virtualdevicecontext.html index d6e3e0c63..1caf05bd2 100644 --- a/netbox/templates/dcim/virtualdevicecontext.html +++ b/netbox/templates/dcim/virtualdevicecontext.html @@ -31,13 +31,23 @@ Primary IPv4 - {{ object.primary_ip4|linkify|placeholder }} + {% if object.primary_ip4 %} + {{ object.primary_ip4 }} + {% copy_content "primary_ip4" %} + {% else %} + + {% endif %} Primary IPv6 - {{ object.primary_ip6|linkify|placeholder }} + {% if object.primary_ip6 %} + {{ object.primary_ip6 }} + {% copy_content "primary_ip6" %} + {% else %} + + {% endif %} diff --git a/netbox/templates/users/api_token.html b/netbox/templates/users/api_token.html index 1a9296704..7fd6f064d 100644 --- a/netbox/templates/users/api_token.html +++ b/netbox/templates/users/api_token.html @@ -8,7 +8,7 @@
{% if not settings.ALLOW_TOKEN_RETRIEVAL %} {% endif %}
@@ -19,9 +19,7 @@ Key
- - - + {% copy_content "token_id" %}
{{ key }}
diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 51fd8aa80..3d3b498ad 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -46,12 +46,13 @@ Primary IPv4 {% if object.primary_ip4 %} - {{ object.primary_ip4.address.ip }} + {{ object.primary_ip4.address.ip }} {% if object.primary_ip4.nat_inside %} (NAT for {{ object.primary_ip4.nat_inside.address.ip }}) {% elif object.primary_ip4.nat_outside.exists %} (NAT: {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} + {% copy_content "primary_ip4" %} {% else %} {{ ''|placeholder }} {% endif %} @@ -61,12 +62,13 @@ Primary IPv6 {% if object.primary_ip6 %} - {{ object.primary_ip6.address.ip }} + {{ object.primary_ip6.address.ip }} {% if object.primary_ip6.nat_inside %} (NAT for {{ object.primary_ip6.nat_inside.address.ip }}) {% elif object.primary_ip6.nat_outside.exists %} (NAT: {% for nat in object.primary_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} + {% copy_content "primary_ip6" %} {% else %} {{ ''|placeholder }} {% endif %} diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 0f1484887..cea50b10f 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -12,9 +12,7 @@ ALLOWED_IPS = """{{ value|join:", " }}""" COPY_BUTTON = """ {% if settings.ALLOW_TOKEN_RETRIEVAL %} - - - + {% copy_content record.pk prefix="token_" color="success" %} {% endif %} """ diff --git a/netbox/utilities/templates/builtins/copy_content.html b/netbox/utilities/templates/builtins/copy_content.html new file mode 100644 index 000000000..9025a71a1 --- /dev/null +++ b/netbox/utilities/templates/builtins/copy_content.html @@ -0,0 +1,3 @@ + + + diff --git a/netbox/utilities/templatetags/builtins/tags.py b/netbox/utilities/templatetags/builtins/tags.py index f9fe5f4e3..35aec1000 100644 --- a/netbox/utilities/templatetags/builtins/tags.py +++ b/netbox/utilities/templatetags/builtins/tags.py @@ -6,6 +6,7 @@ from utilities.utils import dict_to_querydict __all__ = ( 'badge', 'checkmark', + 'copy_content', 'customfield_value', 'tag', ) @@ -79,6 +80,17 @@ def checkmark(value, show_false=True, true='Yes', false='No'): } +@register.inclusion_tag('builtins/copy_content.html') +def copy_content(target, prefix=None, color='primary'): + """ + Display a copy button to copy the content of a field. + """ + return { + 'target': f'#{prefix or ""}{target}', + 'color': f'btn-{color}' + } + + @register.inclusion_tag('builtins/htmx_table.html', takes_context=True) def htmx_table(context, viewname, return_url=None, **kwargs): """ From 7419a8e11285934cd6b90817307d147ee4c26688 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 6 Jul 2023 14:51:28 -0400 Subject: [PATCH 10/15] Closes #11738: Annotate utilization on VLAN groups (#13108) * Update serializers.py * Update vlans.py * Update vlans.py * Update vlangroup.html * Update vlans.py * Update vlans.py * Update serializers.py * adds db annotation to calculate utilization * optimize queries * merge fix * adds round function for utilization to limit decimal * fixed object view annotation * consolidated queryset for utilization * lint fixes * Renamed manager method to annotate_utilization() for consistency with other managers --------- Co-authored-by: Abhimanyu Saharan --- netbox/ipam/api/serializers.py | 3 ++- netbox/ipam/api/views.py | 6 +++--- netbox/ipam/models/vlans.py | 4 +++- netbox/ipam/querysets.py | 15 ++++++++++++++- netbox/ipam/tables/vlans.py | 8 ++++++-- netbox/ipam/views.py | 17 ++++++----------- netbox/templates/ipam/vlangroup.html | 4 ++++ 7 files changed, 38 insertions(+), 19 deletions(-) diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 064452667..1501f16dc 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -218,12 +218,13 @@ class VLANGroupSerializer(NetBoxModelSerializer): scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) scope = serializers.SerializerMethodField(read_only=True) vlan_count = serializers.IntegerField(read_only=True) + utilization = serializers.CharField(read_only=True) class Meta: model = VLANGroup fields = [ 'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'min_vid', 'max_vid', - 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', + 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization' ] validators = [] diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index f432e0e6b..99b4c023d 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -1,5 +1,7 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction +from django.db.models import F +from django.db.models.functions import Round from django.shortcuts import get_object_or_404 from django_pglocks import advisory_lock from drf_spectacular.utils import extend_schema @@ -145,9 +147,7 @@ class FHRPGroupAssignmentViewSet(NetBoxModelViewSet): class VLANGroupViewSet(NetBoxModelViewSet): - queryset = VLANGroup.objects.annotate( - vlan_count=count_related(VLAN, 'group') - ).prefetch_related('tags') + queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') serializer_class = serializers.VLANGroupSerializer filterset_class = filtersets.VLANGroupFilterSet diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index 7d4777da9..da504ded2 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -9,7 +9,7 @@ from django.utils.translation import gettext as _ from dcim.models import Interface from ipam.choices import * from ipam.constants import * -from ipam.querysets import VLANQuerySet +from ipam.querysets import VLANQuerySet, VLANGroupQuerySet from netbox.models import OrganizationalModel, PrimaryModel from virtualization.models import VMInterface @@ -63,6 +63,8 @@ class VLANGroup(OrganizationalModel): help_text=_('Highest permissible ID of a child VLAN') ) + objects = VLANGroupQuerySet.as_manager() + class Meta: ordering = ('name', 'pk') # Name may be non-unique constraints = ( diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py index d6b23b843..39da0c3a2 100644 --- a/netbox/ipam/querysets.py +++ b/netbox/ipam/querysets.py @@ -1,8 +1,10 @@ from django.contrib.contenttypes.models import ContentType -from django.db.models import Count, OuterRef, Q, Subquery, Value +from django.db.models import Count, F, OuterRef, Q, Subquery, Value from django.db.models.expressions import RawSQL +from django.db.models.functions import Round from utilities.querysets import RestrictedQuerySet +from utilities.utils import count_related __all__ = ( 'ASNRangeQuerySet', @@ -54,6 +56,17 @@ class PrefixQuerySet(RestrictedQuerySet): ) +class VLANGroupQuerySet(RestrictedQuerySet): + + def annotate_utilization(self): + from .models import VLAN + + return self.annotate( + vlan_count=count_related(VLAN, 'group'), + utilization=Round(F('vlan_count') / (F('max_vid') - F('min_vid') + 1.0) * 100, 2) + ) + + class VLANQuerySet(RestrictedQuerySet): def get_for_device(self, device): diff --git a/netbox/ipam/tables/vlans.py b/netbox/ipam/tables/vlans.py index 6fa2cd2da..5d9828531 100644 --- a/netbox/ipam/tables/vlans.py +++ b/netbox/ipam/tables/vlans.py @@ -70,6 +70,10 @@ class VLANGroupTable(NetBoxTable): url_params={'group_id': 'pk'}, verbose_name='VLANs' ) + utilization = columns.UtilizationColumn( + orderable=False, + verbose_name='Utilization' + ) tags = columns.TagColumn( url_name='ipam:vlangroup_list' ) @@ -81,9 +85,9 @@ class VLANGroupTable(NetBoxTable): model = VLANGroup fields = ( 'pk', 'id', 'name', 'scope_type', 'scope', 'min_vid', 'max_vid', 'vlan_count', 'slug', 'description', - 'tags', 'created', 'last_updated', 'actions', + 'tags', 'created', 'last_updated', 'actions', 'utilization', ) - default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description') + default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'utilization', 'description') # diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 6efaef8ea..32badd2d5 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes.models import ContentType -from django.db.models import Prefetch +from django.db.models import F, Prefetch from django.db.models.expressions import RawSQL +from django.db.models.functions import Round from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import gettext as _ @@ -882,9 +883,7 @@ class IPAddressRelatedIPsView(generic.ObjectChildrenView): # class VLANGroupListView(generic.ObjectListView): - queryset = VLANGroup.objects.annotate( - vlan_count=count_related(VLAN, 'group') - ) + queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') filterset = filtersets.VLANGroupFilterSet filterset_form = forms.VLANGroupFilterForm table = tables.VLANGroupTable @@ -892,7 +891,7 @@ class VLANGroupListView(generic.ObjectListView): @register_model_view(VLANGroup) class VLANGroupView(generic.ObjectView): - queryset = VLANGroup.objects.all() + queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') def get_extra_context(self, request, instance): related_models = ( @@ -934,18 +933,14 @@ class VLANGroupBulkImportView(generic.BulkImportView): class VLANGroupBulkEditView(generic.BulkEditView): - queryset = VLANGroup.objects.annotate( - vlan_count=count_related(VLAN, 'group') - ) + queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') filterset = filtersets.VLANGroupFilterSet table = tables.VLANGroupTable form = forms.VLANGroupBulkEditForm class VLANGroupBulkDeleteView(generic.BulkDeleteView): - queryset = VLANGroup.objects.annotate( - vlan_count=count_related(VLAN, 'group') - ) + queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') filterset = filtersets.VLANGroupFilterSet table = tables.VLANGroupTable diff --git a/netbox/templates/ipam/vlangroup.html b/netbox/templates/ipam/vlangroup.html index 2917536be..e474cbd84 100644 --- a/netbox/templates/ipam/vlangroup.html +++ b/netbox/templates/ipam/vlangroup.html @@ -42,6 +42,10 @@ Permitted VIDs {{ object.min_vid }} - {{ object.max_vid }} + + Utilization + {% utilization_graph object.utilization %} +
From ecb4a084cc0415421d5030e66655aeb32ed6ea29 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 6 Jul 2023 14:54:37 -0400 Subject: [PATCH 11/15] Change log for #11738, #12499, #12579, #12617, #13047, #13065, #13092, #13100 --- docs/release-notes/version-3.5.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index beb0a1991..194d44922 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -4,13 +4,18 @@ ### Enhancements +* [#11738](https://github.com/netbox-community/netbox/issues/11738) - Annotate VLAN group utilization +* [#12499](https://github.com/netbox-community/netbox/issues/12499) - Add "copy to clipboard" buttons in UI for IP addresses * [#12945](https://github.com/netbox-community/netbox/issues/12945) - Add 100GE QSFP-DD interface type * [#12955](https://github.com/netbox-community/netbox/issues/12955) - Include additional contact details on contact assignments table +* [#13065](https://github.com/netbox-community/netbox/issues/13065) - Associate contact assignments with their objects in the change log ### Bug Fixes * [#11335](https://github.com/netbox-community/netbox/issues/11335) - Exclude stale content types when retrieving changelog records * [#12533](https://github.com/netbox-community/netbox/issues/12533) - Fix REST API validation of null values for several interface attributes +* [#12579](https://github.com/netbox-community/netbox/issues/12579) - Fix exception when clicking "create and add another" to add a cable +* [#12617](https://github.com/netbox-community/netbox/issues/12617) - Populate prechange snapshot on parent object when assigning/removing primary IP address * [#12760](https://github.com/netbox-community/netbox/issues/12760) - Avoid rendering partial HTMX responses when restoring browser tabs * [#12842](https://github.com/netbox-community/netbox/issues/12842) - Improve handling of exceptions when loading reports * [#12849](https://github.com/netbox-community/netbox/issues/12849) - Fix LDAP group permissions assignment for API clients @@ -24,6 +29,9 @@ * [#12983](https://github.com/netbox-community/netbox/issues/12983) - Avoid erroneously clearing many-to-many assignments during bulk edit * [#12989](https://github.com/netbox-community/netbox/issues/12989) - Fix bulk import of tags for device & module types * [#13011](https://github.com/netbox-community/netbox/issues/13011) - Do not escape commas when rendering custom links +* [#13047](https://github.com/netbox-community/netbox/issues/13047) - Correct ASN count under ASN ranges list +* [#13092](https://github.com/netbox-community/netbox/issues/13092) - Allow nullifying power port max & allocated draw values during bulk edit +* [#13100](https://github.com/netbox-community/netbox/issues/13100) - Fix ValueError exception when searching for virtual device context for non-numeric values --- From 74fb707ad3110a3da8cf943520b961118c03264d Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 7 Jul 2023 00:43:02 +0530 Subject: [PATCH 12/15] adds config_template to device serializer #13056 --- netbox/dcim/api/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 2f854d3e4..9cf30fdd4 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -698,7 +698,8 @@ class DeviceWithConfigContextSerializer(DeviceSerializer): 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', - 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated', + 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'config_template', + 'created', 'last_updated', ] @extend_schema_field(serializers.JSONField(allow_null=True)) From 53a75a3dd78909c8586bea5baea74eb9f1214b76 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 6 Jul 2023 16:20:14 -0400 Subject: [PATCH 13/15] Release v3.5.5 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- docs/release-notes/version-3.5.md | 3 ++- netbox/netbox/settings.py | 2 +- requirements.txt | 16 ++++++++-------- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index b3dd583ca..a43f754bf 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.5.4 + placeholder: v3.5.5 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index bd93001e7..c79b2fe9c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.5.4 + placeholder: v3.5.5 validations: required: true - type: dropdown diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 194d44922..6087f895f 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -1,6 +1,6 @@ # NetBox v3.5 -## v3.5.5 (FUTURE) +## v3.5.5 (2023-07-06) ### Enhancements @@ -30,6 +30,7 @@ * [#12989](https://github.com/netbox-community/netbox/issues/12989) - Fix bulk import of tags for device & module types * [#13011](https://github.com/netbox-community/netbox/issues/13011) - Do not escape commas when rendering custom links * [#13047](https://github.com/netbox-community/netbox/issues/13047) - Correct ASN count under ASN ranges list +* [#13056](https://github.com/netbox-community/netbox/issues/13056) - Add `config_template` field to device API serializer * [#13092](https://github.com/netbox-community/netbox/issues/13092) - Allow nullifying power port max & allocated draw values during bulk edit * [#13100](https://github.com/netbox-community/netbox/issues/13100) - Fix ValueError exception when searching for virtual device context for non-numeric values diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 31363144f..8a7133c07 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.5.5-dev' +VERSION = '3.5.5' # Hostname HOSTNAME = platform.node() diff --git a/requirements.txt b/requirements.txt index e6e56ce56..df729d30f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ bleach==6.0.0 -boto3==1.26.156 -Django==4.1.9 +boto3==1.27.1 +Django==4.1.10 django-cors-headers==4.1.0 django-debug-toolbar==4.1.0 django-filter==23.2 @@ -11,25 +11,25 @@ django-prometheus==2.3.1 django-redis==5.3.0 django-rich==1.6.0 django-rq==2.8.1 -django-tables2==2.5.3 +django-tables2==2.6.0 django-taggit==4.0.0 django-timezone-field==5.1 djangorestframework==3.14.0 -drf-spectacular==0.26.2 -drf-spectacular-sidecar==2023.6.1 +drf-spectacular==0.26.3 +drf-spectacular-sidecar==2023.7.1 dulwich==0.21.5 feedparser==6.0.10 graphene-django==3.0.0 gunicorn==20.1.0 Jinja2==3.1.2 Markdown==3.3.7 -mkdocs-material==9.1.16 +mkdocs-material==9.1.18 mkdocstrings[python-legacy]==0.22.0 netaddr==0.8.0 -Pillow==9.5.0 +Pillow==10.0.0 psycopg2-binary==2.9.6 PyYAML==6.0 -sentry-sdk==1.25.1 +sentry-sdk==1.27.1 social-auth-app-django==5.2.0 social-auth-core[openidconnect]==4.4.2 svgwrite==1.4.3 From 63c33ff4bedd29986e189fc35dd5cfd1018c0554 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 6 Jul 2023 16:40:11 -0400 Subject: [PATCH 14/15] PRVB --- docs/release-notes/version-3.5.md | 4 ++++ netbox/netbox/settings.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 6087f895f..678966bc5 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -1,5 +1,9 @@ # NetBox v3.5 +## v3.5.6 (FUTURE) + +--- + ## v3.5.5 (2023-07-06) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 8a7133c07..c260a3a56 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.5.5' +VERSION = '3.5.6-dev' # Hostname HOSTNAME = platform.node() From bc7678c7160ee1746287b8d8ba21134e01c6c499 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 7 Jul 2023 12:16:19 +0530 Subject: [PATCH 15/15] fixes content type lookups when db is uninitialized #13116 --- netbox/extras/querysets.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py index 2e6f93b93..7b71fa656 100644 --- a/netbox/extras/querysets.py +++ b/netbox/extras/querysets.py @@ -2,6 +2,7 @@ from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.aggregates import JSONBAgg from django.db.models import OuterRef, Subquery, Q +from django.db.utils import ProgrammingError from extras.models.tags import TaggedItem from utilities.query_functions import EmptyGroupByJSONBAgg @@ -160,7 +161,13 @@ class ObjectChangeQuerySet(RestrictedQuerySet): def valid_models(self): # Exclude any change records which refer to an instance of a model that's no longer installed. This # can happen when a plugin is removed but its data remains in the database, for example. + try: + content_types = ContentType.objects.get_for_models(*apps.get_models()).values() + except ProgrammingError: + # Handle the case where the database schema has not yet been initialized + content_types = ContentType.objects.none() + content_type_ids = set( - ct.pk for ct in ContentType.objects.get_for_models(*apps.get_models()).values() + ct.pk for ct in content_types ) return self.filter(changed_object_type_id__in=content_type_ids)