From bd144c996a03bafd29c2b806f577a7796cd8d585 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 15 Dec 2020 22:16:26 -0500 Subject: [PATCH 01/25] PRVB --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index b2269ca0e5d..d496f9969f7 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ from django.core.validators import URLValidator # Environment setup # -VERSION = '2.10.1' +VERSION = '2.10.2-dev' # Hostname HOSTNAME = platform.node() From 4ce7dfa55edc0ddc1229c22ad837bcb959c4ec60 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 16 Dec 2020 09:26:22 -0500 Subject: [PATCH 02/25] Fixes #5473: Fix alignment of rack names in elevations list --- docs/release-notes/version-2.10.md | 8 ++++++++ netbox/templates/dcim/rack.html | 4 ++++ netbox/templates/dcim/rack_elevation_list.html | 4 +++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 7cf199e2102..03e2858eb69 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -1,5 +1,13 @@ # NetBox v2.10 +## v2.10.2 (FUTURE) + +### Bug Fixes + +* [#5473](https://github.com/netbox-community/netbox/issues/5473) - Fix alignment of rack names in elevations list + +--- + ## v2.10.1 (2020-12-15) ### Bug Fixes diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 1e0813e5ce3..6a00308f302 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -330,12 +330,16 @@
+

Front

{% include 'dcim/inc/rack_elevation.html' with face='front' %} +
+

Rear

{% include 'dcim/inc/rack_elevation.html' with face='rear' %} +
diff --git a/netbox/templates/dcim/rack_elevation_list.html b/netbox/templates/dcim/rack_elevation_list.html index 1f478284744..a42610e35c9 100644 --- a/netbox/templates/dcim/rack_elevation_list.html +++ b/netbox/templates/dcim/rack_elevation_list.html @@ -25,7 +25,8 @@ {% if page %}
{% for rack in page %} -
+
+
{{ rack.name }} {% if rack.role %} @@ -43,6 +44,7 @@ ({{ rack.facility_id }}) {% endif %}
+
{% endfor %}
From 577c86fb80ca1b2bf35fcf2554b83591a22431a8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 16 Dec 2020 10:38:09 -0500 Subject: [PATCH 03/25] Fixes #5478: Fix display of route target description --- docs/release-notes/version-2.10.md | 1 + netbox/templates/ipam/routetarget.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 03e2858eb69..5c4f9f83be5 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -5,6 +5,7 @@ ### Bug Fixes * [#5473](https://github.com/netbox-community/netbox/issues/5473) - Fix alignment of rack names in elevations list +* [#5478](https://github.com/netbox-community/netbox/issues/5478) - Fix display of route target description --- diff --git a/netbox/templates/ipam/routetarget.html b/netbox/templates/ipam/routetarget.html index 4fa8f44240f..3443d0bf4c8 100644 --- a/netbox/templates/ipam/routetarget.html +++ b/netbox/templates/ipam/routetarget.html @@ -78,7 +78,7 @@ Description - {{ vrf.description|placeholder }} + {{ object.description|placeholder }}
From 2ffdc46f8105ad67f1cdde802bb81c1531f38403 Mon Sep 17 00:00:00 2001 From: Christian Loos Date: Thu, 22 Oct 2020 15:23:54 +0200 Subject: [PATCH 04/25] Fixes #5123: Add tests for custom field select changelog --- netbox/extras/tests/test_changelog.py | 40 +++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/netbox/extras/tests/test_changelog.py b/netbox/extras/tests/test_changelog.py index dbdbb534393..c0732649bc7 100644 --- a/netbox/extras/tests/test_changelog.py +++ b/netbox/extras/tests/test_changelog.py @@ -27,6 +27,16 @@ class ChangeLogViewTest(ModelViewTestCase): cf.save() cf.content_types.set([ct]) + # Create a select custom field on the Site model + cf_select = CustomField( + type=CustomFieldTypeChoices.TYPE_SELECT, + name='my_field_select', + required=False, + choices=['Bar', 'Foo'] + ) + cf_select.save() + cf_select.content_types.set([ct]) + def test_create_object(self): tags = self.create_tags('Tag 1', 'Tag 2') form_data = { @@ -34,6 +44,7 @@ class ChangeLogViewTest(ModelViewTestCase): 'slug': 'test-site-1', 'status': SiteStatusChoices.STATUS_ACTIVE, 'cf_my_field': 'ABC', + 'cf_my_field_select': 'Bar', 'tags': [tag.pk for tag in tags], } @@ -54,6 +65,7 @@ class ChangeLogViewTest(ModelViewTestCase): self.assertEqual(oc_list[0].changed_object, site) self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE) self.assertEqual(oc_list[0].object_data['custom_fields']['my_field'], form_data['cf_my_field']) + self.assertEqual(oc_list[0].object_data['custom_fields']['my_field_select'], form_data['cf_my_field_select']) self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_UPDATE) self.assertEqual(oc_list[1].object_data['tags'], ['Tag 1', 'Tag 2']) @@ -68,6 +80,7 @@ class ChangeLogViewTest(ModelViewTestCase): 'slug': 'test-site-x', 'status': SiteStatusChoices.STATUS_PLANNED, 'cf_my_field': 'DEF', + 'cf_my_field_select': 'Foo', 'tags': [tags[2].pk], } @@ -88,6 +101,7 @@ class ChangeLogViewTest(ModelViewTestCase): self.assertEqual(oc.changed_object, site) self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_UPDATE) self.assertEqual(oc.object_data['custom_fields']['my_field'], form_data['cf_my_field']) + self.assertEqual(oc.object_data['custom_fields']['my_field_select'], form_data['cf_my_field_select']) self.assertEqual(oc.object_data['tags'], ['Tag 3']) def test_delete_object(self): @@ -95,7 +109,8 @@ class ChangeLogViewTest(ModelViewTestCase): name='Test Site 1', slug='test-site-1', custom_field_data={ - 'my_field': 'ABC' + 'my_field': 'ABC', + 'my_field_select': 'Bar' } ) site.save() @@ -115,6 +130,7 @@ class ChangeLogViewTest(ModelViewTestCase): self.assertEqual(oc.object_repr, site.name) self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_DELETE) self.assertEqual(oc.object_data['custom_fields']['my_field'], 'ABC') + self.assertEqual(oc.object_data['custom_fields']['my_field_select'], 'Bar') self.assertEqual(oc.object_data['tags'], ['Tag 1', 'Tag 2']) @@ -133,6 +149,16 @@ class ChangeLogAPITest(APITestCase): cf.save() cf.content_types.set([ct]) + # Create a select custom field on the Site model + cf_select = CustomField( + type=CustomFieldTypeChoices.TYPE_SELECT, + name='my_field_select', + required=False, + choices=['Bar', 'Foo'] + ) + cf_select.save() + cf_select.content_types.set([ct]) + # Create some tags tags = ( Tag(name='Tag 1', slug='tag-1'), @@ -146,7 +172,8 @@ class ChangeLogAPITest(APITestCase): 'name': 'Test Site 1', 'slug': 'test-site-1', 'custom_fields': { - 'my_field': 'ABC' + 'my_field': 'ABC', + 'my_field_select': 'Bar', }, 'tags': [ {'name': 'Tag 1'}, @@ -180,7 +207,8 @@ class ChangeLogAPITest(APITestCase): 'name': 'Test Site X', 'slug': 'test-site-x', 'custom_fields': { - 'my_field': 'DEF' + 'my_field': 'DEF', + 'my_field_select': 'Foo', }, 'tags': [ {'name': 'Tag 3'} @@ -209,7 +237,8 @@ class ChangeLogAPITest(APITestCase): name='Test Site 1', slug='test-site-1', custom_field_data={ - 'my_field': 'ABC' + 'my_field': 'ABC', + 'my_field_select': 'Bar' } ) site.save() @@ -226,5 +255,6 @@ class ChangeLogAPITest(APITestCase): self.assertEqual(oc.changed_object, None) self.assertEqual(oc.object_repr, site.name) self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_DELETE) - self.assertEqual(oc.object_data['custom_fields'], {'my_field': 'ABC'}) + self.assertEqual(oc.object_data['custom_fields']['my_field'], 'ABC') + self.assertEqual(oc.object_data['custom_fields']['my_field_select'], 'Bar') self.assertEqual(oc.object_data['tags'], ['Tag 1', 'Tag 2']) From 52c9e3ecefbe04f5abf7f4d110cc6b3199b1eaca Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 16 Dec 2020 13:23:45 -0500 Subject: [PATCH 05/25] Run CI for pull requests --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d154f5017dd..9182457a02c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,5 @@ name: CI -on: push +on: [push, pull_request] jobs: build: runs-on: ubuntu-latest From 3c887b0dd90e79e507c5f40514d77ea3b0d99399 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 16 Dec 2020 13:48:44 -0500 Subject: [PATCH 06/25] Fixes #5468: Fix unlocking secrets from device/VM view --- docs/release-notes/version-2.10.md | 1 + netbox/templates/secrets/inc/assigned_secrets.html | 3 +++ netbox/templates/virtualization/virtualmachine.html | 1 + 3 files changed, 5 insertions(+) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 5c4f9f83be5..d43f57ac582 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#5468](https://github.com/netbox-community/netbox/issues/5468) - Fix unlocking secrets from device/VM view * [#5473](https://github.com/netbox-community/netbox/issues/5473) - Fix alignment of rack names in elevations list * [#5478](https://github.com/netbox-community/netbox/issues/5478) - Fix display of route target description diff --git a/netbox/templates/secrets/inc/assigned_secrets.html b/netbox/templates/secrets/inc/assigned_secrets.html index 2ff3e4ea268..594ab43f30b 100644 --- a/netbox/templates/secrets/inc/assigned_secrets.html +++ b/netbox/templates/secrets/inc/assigned_secrets.html @@ -1,4 +1,7 @@ {% if secrets %} +
+ {% csrf_token %} +
{% for secret in secrets %} diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 0f4a0416d8a..8baec69561a 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -317,5 +317,6 @@ {% block javascript %} + {% endblock %} From 26426c143143623322ad2a6affb5fd9f5f71ce53 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 17 Dec 2020 09:46:50 -0500 Subject: [PATCH 07/25] Fixes #5484: Fix "tagged" indication in VLAN members list --- docs/release-notes/version-2.10.md | 1 + netbox/ipam/tables.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index d43f57ac582..9fe23d253c7 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -7,6 +7,7 @@ * [#5468](https://github.com/netbox-community/netbox/issues/5468) - Fix unlocking secrets from device/VM view * [#5473](https://github.com/netbox-community/netbox/issues/5473) - Fix alignment of rack names in elevations list * [#5478](https://github.com/netbox-community/netbox/issues/5478) - Fix display of route target description +* [#5484](https://github.com/netbox-community/netbox/issues/5484) - Fix "tagged" indication in VLAN members list --- diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 02196198c0b..868ba310555 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -104,7 +104,7 @@ VLANGROUP_ADD_VLAN = """ """ VLAN_MEMBER_TAGGED = """ -{% if record.untagged_vlan_id == vlan.pk %} +{% if record.untagged_vlan_id == object.pk %} {% else %} From dc6e7cd56c241a278a5c8d5854978a4ac5d5a719 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 17 Dec 2020 09:48:22 -0500 Subject: [PATCH 08/25] Django templating language is no longer supported for export templates --- docs/additional-features/export-templates.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/additional-features/export-templates.md b/docs/additional-features/export-templates.md index b7bbc9842b1..1e0611f0692 100644 --- a/docs/additional-features/export-templates.md +++ b/docs/additional-features/export-templates.md @@ -4,10 +4,7 @@ NetBox allows users to define custom templates that can be used when exporting o Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. -Export templates may be written in Jinja2 or [Django's template language](https://docs.djangoproject.com/en/stable/ref/templates/language/), which is very similar to Jinja2. - -!!! warning - Support for Django's native templating logic will be removed in NetBox v2.10. +Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/). The list of objects returned from the database when rendering an export template is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example: From 202e7d0ebfa1dafc08f4211983200c768efdc1a5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 17 Dec 2020 14:03:10 -0500 Subject: [PATCH 09/25] Fixes #5486: Optimize retrieval of config context data for device/VM REST API views --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/api/views.py | 2 +- netbox/virtualization/api/views.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 9fe23d253c7..719a982ad65 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -8,6 +8,7 @@ * [#5473](https://github.com/netbox-community/netbox/issues/5473) - Fix alignment of rack names in elevations list * [#5478](https://github.com/netbox-community/netbox/issues/5478) - Fix display of route target description * [#5484](https://github.com/netbox-community/netbox/issues/5484) - Fix "tagged" indication in VLAN members list +* [#5486](https://github.com/netbox-community/netbox/issues/5486) - Optimize retrieval of config context data for device/VM REST API views --- diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index db36c31764b..f5149b87691 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -343,7 +343,7 @@ class PlatformViewSet(ModelViewSet): # Devices # -class DeviceViewSet(CustomFieldModelViewSet, ConfigContextQuerySetMixin): +class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet): queryset = Device.objects.prefetch_related( 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay', 'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags', diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index e2d3d5ea5b2..82952ad9a05 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -52,7 +52,7 @@ class ClusterViewSet(CustomFieldModelViewSet): # Virtual machines # -class VirtualMachineViewSet(CustomFieldModelViewSet, ConfigContextQuerySetMixin): +class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet): queryset = VirtualMachine.objects.prefetch_related( 'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags' ) From 91083fd6c52f11a4af163be1978b9a75d66ea639 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 17 Dec 2020 14:45:50 -0500 Subject: [PATCH 10/25] Call Coalesce() inside get_queryset() --- netbox/circuits/api/views.py | 5 ++-- netbox/dcim/api/views.py | 39 +++++++++++++++--------------- netbox/extras/api/views.py | 3 +-- netbox/ipam/api/views.py | 15 ++++++------ netbox/secrets/api/views.py | 3 +-- netbox/tenancy/api/views.py | 15 ++++++------ netbox/utilities/utils.py | 3 ++- netbox/virtualization/api/views.py | 9 +++---- 8 files changed, 43 insertions(+), 49 deletions(-) diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index ef5a944e23c..ad497ee5fb7 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -1,5 +1,4 @@ from django.db.models import Prefetch -from django.db.models.functions import Coalesce from rest_framework.routers import APIRootView from circuits import filters @@ -25,7 +24,7 @@ class CircuitsRootView(APIRootView): class ProviderViewSet(CustomFieldModelViewSet): queryset = Provider.objects.prefetch_related('tags').annotate( - circuit_count=Coalesce(get_subquery(Circuit, 'provider'), 0) + circuit_count=get_subquery(Circuit, 'provider') ) serializer_class = serializers.ProviderSerializer filterset_class = filters.ProviderFilterSet @@ -37,7 +36,7 @@ class ProviderViewSet(CustomFieldModelViewSet): class CircuitTypeViewSet(ModelViewSet): queryset = CircuitType.objects.annotate( - circuit_count=Coalesce(get_subquery(Circuit, 'type'), 0) + circuit_count=get_subquery(Circuit, 'type') ) serializer_class = serializers.CircuitTypeSerializer filterset_class = filters.CircuitTypeFilterSet diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index f5149b87691..efb8e994db7 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -3,7 +3,6 @@ from collections import OrderedDict from django.conf import settings from django.db.models import F -from django.db.models.functions import Coalesce from django.http import HttpResponseForbidden, HttpResponse from django.shortcuts import get_object_or_404 from drf_yasg import openapi @@ -120,12 +119,12 @@ class SiteViewSet(CustomFieldModelViewSet): queryset = Site.objects.prefetch_related( 'region', 'tenant', 'tags' ).annotate( - device_count=Coalesce(get_subquery(Device, 'site'), 0), - rack_count=Coalesce(get_subquery(Rack, 'site'), 0), - prefix_count=Coalesce(get_subquery(Prefix, 'site'), 0), - vlan_count=Coalesce(get_subquery(VLAN, 'site'), 0), - circuit_count=Coalesce(get_subquery(Circuit, 'terminations__site'), 0), - virtualmachine_count=Coalesce(get_subquery(VirtualMachine, 'cluster__site'), 0), + device_count=get_subquery(Device, 'site'), + rack_count=get_subquery(Rack, 'site'), + prefix_count=get_subquery(Prefix, 'site'), + vlan_count=get_subquery(VLAN, 'site'), + circuit_count=get_subquery(Circuit, 'terminations__site'), + virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site') ) serializer_class = serializers.SiteSerializer filterset_class = filters.SiteFilterSet @@ -153,7 +152,7 @@ class RackGroupViewSet(ModelViewSet): class RackRoleViewSet(ModelViewSet): queryset = RackRole.objects.annotate( - rack_count=Coalesce(get_subquery(Rack, 'role'), 0) + rack_count=get_subquery(Rack, 'role') ) serializer_class = serializers.RackRoleSerializer filterset_class = filters.RackRoleFilterSet @@ -167,8 +166,8 @@ class RackViewSet(CustomFieldModelViewSet): queryset = Rack.objects.prefetch_related( 'site', 'group__site', 'role', 'tenant', 'tags' ).annotate( - device_count=Coalesce(get_subquery(Device, 'rack'), 0), - powerfeed_count=Coalesce(get_subquery(PowerFeed, 'rack'), 0) + device_count=get_subquery(Device, 'rack'), + powerfeed_count=get_subquery(PowerFeed, 'rack') ) serializer_class = serializers.RackSerializer filterset_class = filters.RackFilterSet @@ -241,9 +240,9 @@ class RackReservationViewSet(ModelViewSet): class ManufacturerViewSet(ModelViewSet): queryset = Manufacturer.objects.annotate( - devicetype_count=Coalesce(get_subquery(DeviceType, 'manufacturer'), 0), - inventoryitem_count=Coalesce(get_subquery(InventoryItem, 'manufacturer'), 0), - platform_count=Coalesce(get_subquery(Platform, 'manufacturer'), 0) + devicetype_count=get_subquery(DeviceType, 'manufacturer'), + inventoryitem_count=get_subquery(InventoryItem, 'manufacturer'), + platform_count=get_subquery(Platform, 'manufacturer') ) serializer_class = serializers.ManufacturerSerializer filterset_class = filters.ManufacturerFilterSet @@ -255,7 +254,7 @@ class ManufacturerViewSet(ModelViewSet): class DeviceTypeViewSet(CustomFieldModelViewSet): queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate( - device_count=Coalesce(get_subquery(Device, 'device_type'), 0) + device_count=get_subquery(Device, 'device_type') ) serializer_class = serializers.DeviceTypeSerializer filterset_class = filters.DeviceTypeFilterSet @@ -319,8 +318,8 @@ class DeviceBayTemplateViewSet(ModelViewSet): class DeviceRoleViewSet(ModelViewSet): queryset = DeviceRole.objects.annotate( - device_count=Coalesce(get_subquery(Device, 'device_role'), 0), - virtualmachine_count=Coalesce(get_subquery(VirtualMachine, 'role'), 0) + device_count=get_subquery(Device, 'device_role'), + virtualmachine_count=get_subquery(VirtualMachine, 'role') ) serializer_class = serializers.DeviceRoleSerializer filterset_class = filters.DeviceRoleFilterSet @@ -332,8 +331,8 @@ class DeviceRoleViewSet(ModelViewSet): class PlatformViewSet(ModelViewSet): queryset = Platform.objects.annotate( - device_count=Coalesce(get_subquery(Device, 'platform'), 0), - virtualmachine_count=Coalesce(get_subquery(VirtualMachine, 'platform'), 0) + device_count=get_subquery(Device, 'platform'), + virtualmachine_count=get_subquery(VirtualMachine, 'platform') ) serializer_class = serializers.PlatformSerializer filterset_class = filters.PlatformFilterSet @@ -597,7 +596,7 @@ class CableViewSet(ModelViewSet): class VirtualChassisViewSet(ModelViewSet): queryset = VirtualChassis.objects.prefetch_related('tags').annotate( - member_count=Coalesce(get_subquery(Device, 'virtual_chassis'), 0) + member_count=get_subquery(Device, 'virtual_chassis') ) serializer_class = serializers.VirtualChassisSerializer filterset_class = filters.VirtualChassisFilterSet @@ -611,7 +610,7 @@ class PowerPanelViewSet(ModelViewSet): queryset = PowerPanel.objects.prefetch_related( 'site', 'rack_group' ).annotate( - powerfeed_count=Coalesce(get_subquery(PowerFeed, 'power_panel'), 0) + powerfeed_count=get_subquery(PowerFeed, 'power_panel') ) serializer_class = serializers.PowerPanelSerializer filterset_class = filters.PowerPanelFilterSet diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index fcd9add7cc3..38077c89a65 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,5 +1,4 @@ from django.contrib.contenttypes.models import ContentType -from django.db.models.functions import Coalesce from django.http import Http404 from django_rq.queues import get_connection from rest_framework import status @@ -103,7 +102,7 @@ class ExportTemplateViewSet(ModelViewSet): class TagViewSet(ModelViewSet): queryset = Tag.objects.annotate( - tagged_items=Coalesce(get_subquery(TaggedItem, 'tag'), 0) + tagged_items=get_subquery(TaggedItem, 'tag') ) serializer_class = serializers.TagSerializer filterset_class = filters.TagFilterSet diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index fb38edf464c..9d09bbe03cb 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -1,5 +1,4 @@ from django.conf import settings -from django.db.models.functions import Coalesce from django.shortcuts import get_object_or_404 from django_pglocks import advisory_lock from drf_yasg.utils import swagger_auto_schema @@ -33,8 +32,8 @@ class VRFViewSet(CustomFieldModelViewSet): queryset = VRF.objects.prefetch_related('tenant').prefetch_related( 'import_targets', 'export_targets', 'tags' ).annotate( - ipaddress_count=Coalesce(get_subquery(IPAddress, 'vrf'), 0), - prefix_count=Coalesce(get_subquery(Prefix, 'vrf'), 0) + ipaddress_count=get_subquery(IPAddress, 'vrf'), + prefix_count=get_subquery(Prefix, 'vrf') ) serializer_class = serializers.VRFSerializer filterset_class = filters.VRFFilterSet @@ -56,7 +55,7 @@ class RouteTargetViewSet(CustomFieldModelViewSet): class RIRViewSet(ModelViewSet): queryset = RIR.objects.annotate( - aggregate_count=Coalesce(get_subquery(Aggregate, 'rir'), 0) + aggregate_count=get_subquery(Aggregate, 'rir') ) serializer_class = serializers.RIRSerializer filterset_class = filters.RIRFilterSet @@ -78,8 +77,8 @@ class AggregateViewSet(CustomFieldModelViewSet): class RoleViewSet(ModelViewSet): queryset = Role.objects.annotate( - prefix_count=Coalesce(get_subquery(Prefix, 'role'), 0), - vlan_count=Coalesce(get_subquery(VLAN, 'role'), 0) + prefix_count=get_subquery(Prefix, 'role'), + vlan_count=get_subquery(VLAN, 'role') ) serializer_class = serializers.RoleSerializer filterset_class = filters.RoleFilterSet @@ -273,7 +272,7 @@ class IPAddressViewSet(CustomFieldModelViewSet): class VLANGroupViewSet(ModelViewSet): queryset = VLANGroup.objects.prefetch_related('site').annotate( - vlan_count=Coalesce(get_subquery(VLAN, 'group'), 0) + vlan_count=get_subquery(VLAN, 'group') ) serializer_class = serializers.VLANGroupSerializer filterset_class = filters.VLANGroupFilterSet @@ -287,7 +286,7 @@ class VLANViewSet(CustomFieldModelViewSet): queryset = VLAN.objects.prefetch_related( 'site', 'group', 'tenant', 'role', 'tags' ).annotate( - prefix_count=Coalesce(get_subquery(Prefix, 'vlan'), 0) + prefix_count=get_subquery(Prefix, 'vlan') ) serializer_class = serializers.VLANSerializer filterset_class = filters.VLANFilterSet diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 617da5c6e58..1153b0508f3 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -1,7 +1,6 @@ import base64 from Crypto.PublicKey import RSA -from django.db.models.functions import Coalesce from django.http import HttpResponseBadRequest from rest_framework.exceptions import ValidationError from rest_framework.permissions import IsAuthenticated @@ -36,7 +35,7 @@ class SecretsRootView(APIRootView): class SecretRoleViewSet(ModelViewSet): queryset = SecretRole.objects.annotate( - secret_count=Coalesce(get_subquery(Secret, 'role'), 0) + secret_count=get_subquery(Secret, 'role') ) serializer_class = serializers.SecretRoleSerializer filterset_class = filters.SecretRoleFilterSet diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index 142203b5801..34be4991e21 100644 --- a/netbox/tenancy/api/views.py +++ b/netbox/tenancy/api/views.py @@ -1,4 +1,3 @@ -from django.db.models.functions import Coalesce from rest_framework.routers import APIRootView from circuits.models import Circuit @@ -47,13 +46,13 @@ class TenantViewSet(CustomFieldModelViewSet): ).annotate( circuit_count=get_subquery(Circuit, 'tenant'), device_count=get_subquery(Device, 'tenant'), - ipaddress_count=Coalesce(get_subquery(IPAddress, 'tenant'), 0), - prefix_count=Coalesce(get_subquery(Prefix, 'tenant'), 0), - rack_count=Coalesce(get_subquery(Rack, 'tenant'), 0), - site_count=Coalesce(get_subquery(Site, 'tenant'), 0), - virtualmachine_count=Coalesce(get_subquery(VirtualMachine, 'tenant'), 0), - vlan_count=Coalesce(get_subquery(VLAN, 'tenant'), 0), - vrf_count=Coalesce(get_subquery(VRF, 'tenant'), 0) + ipaddress_count=get_subquery(IPAddress, 'tenant'), + prefix_count=get_subquery(Prefix, 'tenant'), + rack_count=get_subquery(Rack, 'tenant'), + site_count=get_subquery(Site, 'tenant'), + virtualmachine_count=get_subquery(VirtualMachine, 'tenant'), + vlan_count=get_subquery(VLAN, 'tenant'), + vrf_count=get_subquery(VRF, 'tenant') ) serializer_class = serializers.TenantSerializer filterset_class = filters.TenantFilterSet diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 52a9515551b..19e08dfd4a5 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -5,6 +5,7 @@ from itertools import count, groupby from django.core.serializers import serialize from django.db.models import Count, OuterRef, Subquery +from django.db.models.functions import Coalesce from jinja2 import Environment from dcim.choices import CableLengthUnitChoices @@ -79,7 +80,7 @@ def get_subquery(model, field): ).values('c') ) - return subquery + return Coalesce(subquery, 0) def serialize_object(obj, extra=None, exclude=None): diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index 82952ad9a05..a3dea00df3c 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -1,4 +1,3 @@ -from django.db.models.functions import Coalesce from rest_framework.routers import APIRootView from dcim.models import Device @@ -23,7 +22,7 @@ class VirtualizationRootView(APIRootView): class ClusterTypeViewSet(ModelViewSet): queryset = ClusterType.objects.annotate( - cluster_count=Coalesce(get_subquery(Cluster, 'type'), 0) + cluster_count=get_subquery(Cluster, 'type') ) serializer_class = serializers.ClusterTypeSerializer filterset_class = filters.ClusterTypeFilterSet @@ -31,7 +30,7 @@ class ClusterTypeViewSet(ModelViewSet): class ClusterGroupViewSet(ModelViewSet): queryset = ClusterGroup.objects.annotate( - cluster_count=Coalesce(get_subquery(Cluster, 'group'), 0) + cluster_count=get_subquery(Cluster, 'group') ) serializer_class = serializers.ClusterGroupSerializer filterset_class = filters.ClusterGroupFilterSet @@ -41,8 +40,8 @@ class ClusterViewSet(CustomFieldModelViewSet): queryset = Cluster.objects.prefetch_related( 'type', 'group', 'tenant', 'site', 'tags' ).annotate( - device_count=Coalesce(get_subquery(Device, 'cluster'), 0), - virtualmachine_count=Coalesce(get_subquery(VirtualMachine, 'cluster'), 0) + device_count=get_subquery(Device, 'cluster'), + virtualmachine_count=get_subquery(VirtualMachine, 'cluster') ) serializer_class = serializers.ClusterSerializer filterset_class = filters.ClusterFilterSet From 4a066a191318799a48b4e5380feea6774e4ea8fe Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 17 Dec 2020 14:47:49 -0500 Subject: [PATCH 11/25] Rename get_subquery() to count_related() --- netbox/circuits/api/views.py | 6 ++--- netbox/circuits/views.py | 12 ++++----- netbox/dcim/api/views.py | 40 +++++++++++++++--------------- netbox/dcim/views.py | 36 +++++++++++++-------------- netbox/extras/api/views.py | 4 +-- netbox/extras/views.py | 8 +++--- netbox/ipam/api/views.py | 16 ++++++------ netbox/ipam/views.py | 14 +++++------ netbox/netbox/constants.py | 12 ++++----- netbox/secrets/api/views.py | 4 +-- netbox/secrets/views.py | 6 ++--- netbox/tenancy/api/views.py | 20 +++++++-------- netbox/utilities/utils.py | 2 +- netbox/virtualization/api/views.py | 10 ++++---- netbox/virtualization/views.py | 14 +++++------ 15 files changed, 102 insertions(+), 102 deletions(-) diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index ad497ee5fb7..6968da61e6c 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -6,7 +6,7 @@ from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from dcim.api.views import PathEndpointMixin from extras.api.views import CustomFieldModelViewSet from netbox.api.views import ModelViewSet -from utilities.utils import get_subquery +from utilities.utils import count_related from . import serializers @@ -24,7 +24,7 @@ class CircuitsRootView(APIRootView): class ProviderViewSet(CustomFieldModelViewSet): queryset = Provider.objects.prefetch_related('tags').annotate( - circuit_count=get_subquery(Circuit, 'provider') + circuit_count=count_related(Circuit, 'provider') ) serializer_class = serializers.ProviderSerializer filterset_class = filters.ProviderFilterSet @@ -36,7 +36,7 @@ class ProviderViewSet(CustomFieldModelViewSet): class CircuitTypeViewSet(ModelViewSet): queryset = CircuitType.objects.annotate( - circuit_count=get_subquery(Circuit, 'type') + circuit_count=count_related(Circuit, 'type') ) serializer_class = serializers.CircuitTypeSerializer filterset_class = filters.CircuitTypeFilterSet diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index a237b88058a..9fea2665220 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -6,7 +6,7 @@ from django_tables2 import RequestConfig from netbox.views import generic from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator, get_paginate_count -from utilities.utils import get_subquery +from utilities.utils import count_related from . import filters, forms, tables from .choices import CircuitTerminationSideChoices from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -18,7 +18,7 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider class ProviderListView(generic.ObjectListView): queryset = Provider.objects.annotate( - count_circuits=get_subquery(Circuit, 'provider') + count_circuits=count_related(Circuit, 'provider') ) filterset = filters.ProviderFilterSet filterset_form = forms.ProviderFilterForm @@ -67,7 +67,7 @@ class ProviderBulkImportView(generic.BulkImportView): class ProviderBulkEditView(generic.BulkEditView): queryset = Provider.objects.annotate( - count_circuits=get_subquery(Circuit, 'provider') + count_circuits=count_related(Circuit, 'provider') ) filterset = filters.ProviderFilterSet table = tables.ProviderTable @@ -76,7 +76,7 @@ class ProviderBulkEditView(generic.BulkEditView): class ProviderBulkDeleteView(generic.BulkDeleteView): queryset = Provider.objects.annotate( - count_circuits=get_subquery(Circuit, 'provider') + count_circuits=count_related(Circuit, 'provider') ) filterset = filters.ProviderFilterSet table = tables.ProviderTable @@ -88,7 +88,7 @@ class ProviderBulkDeleteView(generic.BulkDeleteView): class CircuitTypeListView(generic.ObjectListView): queryset = CircuitType.objects.annotate( - circuit_count=get_subquery(Circuit, 'type') + circuit_count=count_related(Circuit, 'type') ) table = tables.CircuitTypeTable @@ -110,7 +110,7 @@ class CircuitTypeBulkImportView(generic.BulkImportView): class CircuitTypeBulkDeleteView(generic.BulkDeleteView): queryset = CircuitType.objects.annotate( - circuit_count=get_subquery(Circuit, 'type') + circuit_count=count_related(Circuit, 'type') ) table = tables.CircuitTypeTable diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index efb8e994db7..f9e8027b45f 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -30,7 +30,7 @@ from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired from netbox.api.exceptions import ServiceUnavailable from netbox.api.metadata import ContentTypeMetadata from utilities.api import get_serializer_for_model -from utilities.utils import get_subquery +from utilities.utils import count_related from virtualization.models import VirtualMachine from . import serializers from .exceptions import MissingFilterException @@ -119,12 +119,12 @@ class SiteViewSet(CustomFieldModelViewSet): queryset = Site.objects.prefetch_related( 'region', 'tenant', 'tags' ).annotate( - device_count=get_subquery(Device, 'site'), - rack_count=get_subquery(Rack, 'site'), - prefix_count=get_subquery(Prefix, 'site'), - vlan_count=get_subquery(VLAN, 'site'), - circuit_count=get_subquery(Circuit, 'terminations__site'), - virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site') + device_count=count_related(Device, 'site'), + rack_count=count_related(Rack, 'site'), + prefix_count=count_related(Prefix, 'site'), + vlan_count=count_related(VLAN, 'site'), + circuit_count=count_related(Circuit, 'terminations__site'), + virtualmachine_count=count_related(VirtualMachine, 'cluster__site') ) serializer_class = serializers.SiteSerializer filterset_class = filters.SiteFilterSet @@ -152,7 +152,7 @@ class RackGroupViewSet(ModelViewSet): class RackRoleViewSet(ModelViewSet): queryset = RackRole.objects.annotate( - rack_count=get_subquery(Rack, 'role') + rack_count=count_related(Rack, 'role') ) serializer_class = serializers.RackRoleSerializer filterset_class = filters.RackRoleFilterSet @@ -166,8 +166,8 @@ class RackViewSet(CustomFieldModelViewSet): queryset = Rack.objects.prefetch_related( 'site', 'group__site', 'role', 'tenant', 'tags' ).annotate( - device_count=get_subquery(Device, 'rack'), - powerfeed_count=get_subquery(PowerFeed, 'rack') + device_count=count_related(Device, 'rack'), + powerfeed_count=count_related(PowerFeed, 'rack') ) serializer_class = serializers.RackSerializer filterset_class = filters.RackFilterSet @@ -240,9 +240,9 @@ class RackReservationViewSet(ModelViewSet): class ManufacturerViewSet(ModelViewSet): queryset = Manufacturer.objects.annotate( - devicetype_count=get_subquery(DeviceType, 'manufacturer'), - inventoryitem_count=get_subquery(InventoryItem, 'manufacturer'), - platform_count=get_subquery(Platform, 'manufacturer') + devicetype_count=count_related(DeviceType, 'manufacturer'), + inventoryitem_count=count_related(InventoryItem, 'manufacturer'), + platform_count=count_related(Platform, 'manufacturer') ) serializer_class = serializers.ManufacturerSerializer filterset_class = filters.ManufacturerFilterSet @@ -254,7 +254,7 @@ class ManufacturerViewSet(ModelViewSet): class DeviceTypeViewSet(CustomFieldModelViewSet): queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate( - device_count=get_subquery(Device, 'device_type') + device_count=count_related(Device, 'device_type') ) serializer_class = serializers.DeviceTypeSerializer filterset_class = filters.DeviceTypeFilterSet @@ -318,8 +318,8 @@ class DeviceBayTemplateViewSet(ModelViewSet): class DeviceRoleViewSet(ModelViewSet): queryset = DeviceRole.objects.annotate( - device_count=get_subquery(Device, 'device_role'), - virtualmachine_count=get_subquery(VirtualMachine, 'role') + device_count=count_related(Device, 'device_role'), + virtualmachine_count=count_related(VirtualMachine, 'role') ) serializer_class = serializers.DeviceRoleSerializer filterset_class = filters.DeviceRoleFilterSet @@ -331,8 +331,8 @@ class DeviceRoleViewSet(ModelViewSet): class PlatformViewSet(ModelViewSet): queryset = Platform.objects.annotate( - device_count=get_subquery(Device, 'platform'), - virtualmachine_count=get_subquery(VirtualMachine, 'platform') + device_count=count_related(Device, 'platform'), + virtualmachine_count=count_related(VirtualMachine, 'platform') ) serializer_class = serializers.PlatformSerializer filterset_class = filters.PlatformFilterSet @@ -596,7 +596,7 @@ class CableViewSet(ModelViewSet): class VirtualChassisViewSet(ModelViewSet): queryset = VirtualChassis.objects.prefetch_related('tags').annotate( - member_count=get_subquery(Device, 'virtual_chassis') + member_count=count_related(Device, 'virtual_chassis') ) serializer_class = serializers.VirtualChassisSerializer filterset_class = filters.VirtualChassisFilterSet @@ -610,7 +610,7 @@ class PowerPanelViewSet(ModelViewSet): queryset = PowerPanel.objects.prefetch_related( 'site', 'rack_group' ).annotate( - powerfeed_count=get_subquery(PowerFeed, 'power_panel') + powerfeed_count=count_related(PowerFeed, 'power_panel') ) serializer_class = serializers.PowerPanelSerializer filterset_class = filters.PowerPanelFilterSet diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 9f8e4c13fa8..b092be61249 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -20,7 +20,7 @@ from secrets.models import Secret from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.permissions import get_permission_for_model -from utilities.utils import csv_format, get_subquery +from utilities.utils import csv_format, count_related from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin from virtualization.models import VirtualMachine from . import filters, forms, tables @@ -254,7 +254,7 @@ class RackGroupBulkDeleteView(generic.BulkDeleteView): class RackRoleListView(generic.ObjectListView): queryset = RackRole.objects.annotate( - rack_count=get_subquery(Rack, 'role') + rack_count=count_related(Rack, 'role') ) table = tables.RackRoleTable @@ -276,7 +276,7 @@ class RackRoleBulkImportView(generic.BulkImportView): class RackRoleBulkDeleteView(generic.BulkDeleteView): queryset = RackRole.objects.annotate( - rack_count=get_subquery(Rack, 'role') + rack_count=count_related(Rack, 'role') ) table = tables.RackRoleTable @@ -289,7 +289,7 @@ class RackListView(generic.ObjectListView): queryset = Rack.objects.prefetch_related( 'site', 'group', 'tenant', 'role', 'devices__device_type' ).annotate( - device_count=get_subquery(Device, 'rack') + device_count=count_related(Device, 'rack') ) filterset = filters.RackFilterSet filterset_form = forms.RackFilterForm @@ -470,9 +470,9 @@ class RackReservationBulkDeleteView(generic.BulkDeleteView): class ManufacturerListView(generic.ObjectListView): queryset = Manufacturer.objects.annotate( - devicetype_count=get_subquery(DeviceType, 'manufacturer'), - inventoryitem_count=get_subquery(InventoryItem, 'manufacturer'), - platform_count=get_subquery(Platform, 'manufacturer') + devicetype_count=count_related(DeviceType, 'manufacturer'), + inventoryitem_count=count_related(InventoryItem, 'manufacturer'), + platform_count=count_related(Platform, 'manufacturer') ) table = tables.ManufacturerTable @@ -494,7 +494,7 @@ class ManufacturerBulkImportView(generic.BulkImportView): class ManufacturerBulkDeleteView(generic.BulkDeleteView): queryset = Manufacturer.objects.annotate( - devicetype_count=get_subquery(DeviceType, 'manufacturer') + devicetype_count=count_related(DeviceType, 'manufacturer') ) table = tables.ManufacturerTable @@ -505,7 +505,7 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView): class DeviceTypeListView(generic.ObjectListView): queryset = DeviceType.objects.prefetch_related('manufacturer').annotate( - instance_count=get_subquery(Device, 'device_type') + instance_count=count_related(Device, 'device_type') ) filterset = filters.DeviceTypeFilterSet filterset_form = forms.DeviceTypeFilterForm @@ -612,7 +612,7 @@ class DeviceTypeImportView(generic.ObjectImportView): class DeviceTypeBulkEditView(generic.BulkEditView): queryset = DeviceType.objects.prefetch_related('manufacturer').annotate( - instance_count=get_subquery(Device, 'device_type') + instance_count=count_related(Device, 'device_type') ) filterset = filters.DeviceTypeFilterSet table = tables.DeviceTypeTable @@ -621,7 +621,7 @@ class DeviceTypeBulkEditView(generic.BulkEditView): class DeviceTypeBulkDeleteView(generic.BulkDeleteView): queryset = DeviceType.objects.prefetch_related('manufacturer').annotate( - instance_count=get_subquery(Device, 'device_type') + instance_count=count_related(Device, 'device_type') ) filterset = filters.DeviceTypeFilterSet table = tables.DeviceTypeTable @@ -913,8 +913,8 @@ class DeviceBayTemplateBulkDeleteView(generic.BulkDeleteView): class DeviceRoleListView(generic.ObjectListView): queryset = DeviceRole.objects.annotate( - device_count=get_subquery(Device, 'device_role'), - vm_count=get_subquery(VirtualMachine, 'role') + device_count=count_related(Device, 'device_role'), + vm_count=count_related(VirtualMachine, 'role') ) table = tables.DeviceRoleTable @@ -945,8 +945,8 @@ class DeviceRoleBulkDeleteView(generic.BulkDeleteView): class PlatformListView(generic.ObjectListView): queryset = Platform.objects.annotate( - device_count=get_subquery(Device, 'platform'), - vm_count=get_subquery(VirtualMachine, 'platform') + device_count=count_related(Device, 'platform'), + vm_count=count_related(VirtualMachine, 'platform') ) table = tables.PlatformTable @@ -2335,7 +2335,7 @@ class InterfaceConnectionsListView(generic.ObjectListView): class VirtualChassisListView(generic.ObjectListView): queryset = VirtualChassis.objects.prefetch_related('master').annotate( - member_count=get_subquery(Device, 'virtual_chassis') + member_count=count_related(Device, 'virtual_chassis') ) table = tables.VirtualChassisTable filterset = filters.VirtualChassisFilterSet @@ -2565,7 +2565,7 @@ class PowerPanelListView(generic.ObjectListView): queryset = PowerPanel.objects.prefetch_related( 'site', 'rack_group' ).annotate( - powerfeed_count=get_subquery(PowerFeed, 'power_panel') + powerfeed_count=count_related(PowerFeed, 'power_panel') ) filterset = filters.PowerPanelFilterSet filterset_form = forms.PowerPanelFilterForm @@ -2615,7 +2615,7 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView): queryset = PowerPanel.objects.prefetch_related( 'site', 'rack_group' ).annotate( - powerfeed_count=get_subquery(PowerFeed, 'power_panel') + powerfeed_count=count_related(PowerFeed, 'power_panel') ) filterset = filters.PowerPanelFilterSet table = tables.PowerPanelTable diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 38077c89a65..8ab7b0eeae6 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -21,7 +21,7 @@ from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired from netbox.api.metadata import ContentTypeMetadata from netbox.api.views import ModelViewSet from utilities.exceptions import RQWorkerNotRunningException -from utilities.utils import copy_safe_request, get_subquery +from utilities.utils import copy_safe_request, count_related from . import serializers @@ -102,7 +102,7 @@ class ExportTemplateViewSet(ModelViewSet): class TagViewSet(ModelViewSet): queryset = Tag.objects.annotate( - tagged_items=get_subquery(TaggedItem, 'tag') + tagged_items=count_related(TaggedItem, 'tag') ) serializer_class = serializers.TagSerializer filterset_class = filters.TagFilterSet diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 99295de1ae9..57483345c77 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -12,7 +12,7 @@ from rq import Worker from netbox.views import generic from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator, get_paginate_count -from utilities.utils import copy_safe_request, get_subquery, shallow_compare_dict +from utilities.utils import copy_safe_request, count_related, shallow_compare_dict from utilities.views import ContentTypePermissionRequiredMixin from . import filters, forms, tables from .choices import JobResultStatusChoices @@ -27,7 +27,7 @@ from .scripts import get_scripts, run_script class TagListView(generic.ObjectListView): queryset = Tag.objects.annotate( - items=get_subquery(TaggedItem, 'tag') + items=count_related(TaggedItem, 'tag') ) filterset = filters.TagFilterSet filterset_form = forms.TagFilterForm @@ -52,7 +52,7 @@ class TagBulkImportView(generic.BulkImportView): class TagBulkEditView(generic.BulkEditView): queryset = Tag.objects.annotate( - items=get_subquery(TaggedItem, 'tag') + items=count_related(TaggedItem, 'tag') ) table = tables.TagTable form = forms.TagBulkEditForm @@ -60,7 +60,7 @@ class TagBulkEditView(generic.BulkEditView): class TagBulkDeleteView(generic.BulkDeleteView): queryset = Tag.objects.annotate( - items=get_subquery(TaggedItem, 'tag') + items=count_related(TaggedItem, 'tag') ) table = tables.TagTable diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 9d09bbe03cb..d9eae69aa50 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -12,7 +12,7 @@ from ipam import filters from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF from netbox.api.views import ModelViewSet from utilities.constants import ADVISORY_LOCK_KEYS -from utilities.utils import get_subquery +from utilities.utils import count_related from . import serializers @@ -32,8 +32,8 @@ class VRFViewSet(CustomFieldModelViewSet): queryset = VRF.objects.prefetch_related('tenant').prefetch_related( 'import_targets', 'export_targets', 'tags' ).annotate( - ipaddress_count=get_subquery(IPAddress, 'vrf'), - prefix_count=get_subquery(Prefix, 'vrf') + ipaddress_count=count_related(IPAddress, 'vrf'), + prefix_count=count_related(Prefix, 'vrf') ) serializer_class = serializers.VRFSerializer filterset_class = filters.VRFFilterSet @@ -55,7 +55,7 @@ class RouteTargetViewSet(CustomFieldModelViewSet): class RIRViewSet(ModelViewSet): queryset = RIR.objects.annotate( - aggregate_count=get_subquery(Aggregate, 'rir') + aggregate_count=count_related(Aggregate, 'rir') ) serializer_class = serializers.RIRSerializer filterset_class = filters.RIRFilterSet @@ -77,8 +77,8 @@ class AggregateViewSet(CustomFieldModelViewSet): class RoleViewSet(ModelViewSet): queryset = Role.objects.annotate( - prefix_count=get_subquery(Prefix, 'role'), - vlan_count=get_subquery(VLAN, 'role') + prefix_count=count_related(Prefix, 'role'), + vlan_count=count_related(VLAN, 'role') ) serializer_class = serializers.RoleSerializer filterset_class = filters.RoleFilterSet @@ -272,7 +272,7 @@ class IPAddressViewSet(CustomFieldModelViewSet): class VLANGroupViewSet(ModelViewSet): queryset = VLANGroup.objects.prefetch_related('site').annotate( - vlan_count=get_subquery(VLAN, 'group') + vlan_count=count_related(VLAN, 'group') ) serializer_class = serializers.VLANGroupSerializer filterset_class = filters.VLANGroupFilterSet @@ -286,7 +286,7 @@ class VLANViewSet(CustomFieldModelViewSet): queryset = VLAN.objects.prefetch_related( 'site', 'group', 'tenant', 'role', 'tags' ).annotate( - prefix_count=get_subquery(Prefix, 'vlan') + prefix_count=count_related(Prefix, 'vlan') ) serializer_class = serializers.VLANSerializer filterset_class = filters.VLANFilterSet diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 1cbac27f653..36c22504560 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -6,7 +6,7 @@ from django_tables2 import RequestConfig from dcim.models import Device, Interface from netbox.views import generic from utilities.paginator import EnhancedPaginator, get_paginate_count -from utilities.utils import get_subquery +from utilities.utils import count_related from virtualization.models import VirtualMachine, VMInterface from . import filters, forms, tables from .constants import * @@ -140,7 +140,7 @@ class RouteTargetBulkDeleteView(generic.BulkDeleteView): class RIRListView(generic.ObjectListView): queryset = RIR.objects.annotate( - aggregate_count=get_subquery(Aggregate, 'rir') + aggregate_count=count_related(Aggregate, 'rir') ) filterset = filters.RIRFilterSet filterset_form = forms.RIRFilterForm @@ -165,7 +165,7 @@ class RIRBulkImportView(generic.BulkImportView): class RIRBulkDeleteView(generic.BulkDeleteView): queryset = RIR.objects.annotate( - aggregate_count=get_subquery(Aggregate, 'rir') + aggregate_count=count_related(Aggregate, 'rir') ) filterset = filters.RIRFilterSet table = tables.RIRTable @@ -277,8 +277,8 @@ class AggregateBulkDeleteView(generic.BulkDeleteView): class RoleListView(generic.ObjectListView): queryset = Role.objects.annotate( - prefix_count=get_subquery(Prefix, 'role'), - vlan_count=get_subquery(VLAN, 'role') + prefix_count=count_related(Prefix, 'role'), + vlan_count=count_related(VLAN, 'role') ) table = tables.RoleTable @@ -633,7 +633,7 @@ class IPAddressBulkDeleteView(generic.BulkDeleteView): class VLANGroupListView(generic.ObjectListView): queryset = VLANGroup.objects.prefetch_related('site').annotate( - vlan_count=get_subquery(VLAN, 'group') + vlan_count=count_related(VLAN, 'group') ) filterset = filters.VLANGroupFilterSet filterset_form = forms.VLANGroupFilterForm @@ -657,7 +657,7 @@ class VLANGroupBulkImportView(generic.BulkImportView): class VLANGroupBulkDeleteView(generic.BulkDeleteView): queryset = VLANGroup.objects.prefetch_related('site').annotate( - vlan_count=get_subquery(VLAN, 'group') + vlan_count=count_related(VLAN, 'group') ) filterset = filters.VLANGroupFilterSet table = tables.VLANGroupTable diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py index a074bde4e2a..4c6e3103a14 100644 --- a/netbox/netbox/constants.py +++ b/netbox/netbox/constants.py @@ -23,7 +23,7 @@ from secrets.tables import SecretTable from tenancy.filters import TenantFilterSet from tenancy.models import Tenant from tenancy.tables import TenantTable -from utilities.utils import get_subquery +from utilities.utils import count_related from virtualization.filters import ClusterFilterSet, VirtualMachineFilterSet from virtualization.models import Cluster, VirtualMachine from virtualization.tables import ClusterTable, VirtualMachineDetailTable @@ -33,7 +33,7 @@ SEARCH_TYPES = OrderedDict(( # Circuits ('provider', { 'queryset': Provider.objects.annotate( - count_circuits=get_subquery(Circuit, 'provider') + count_circuits=count_related(Circuit, 'provider') ), 'filterset': ProviderFilterSet, 'table': ProviderTable, @@ -74,7 +74,7 @@ SEARCH_TYPES = OrderedDict(( }), ('devicetype', { 'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate( - instance_count=get_subquery(Device, 'device_type') + instance_count=count_related(Device, 'device_type') ), 'filterset': DeviceTypeFilterSet, 'table': DeviceTypeTable, @@ -90,7 +90,7 @@ SEARCH_TYPES = OrderedDict(( }), ('virtualchassis', { 'queryset': VirtualChassis.objects.prefetch_related('master').annotate( - member_count=get_subquery(Device, 'virtual_chassis') + member_count=count_related(Device, 'virtual_chassis') ), 'filterset': VirtualChassisFilterSet, 'table': VirtualChassisTable, @@ -111,8 +111,8 @@ SEARCH_TYPES = OrderedDict(( # Virtualization ('cluster', { 'queryset': Cluster.objects.prefetch_related('type', 'group').annotate( - device_count=get_subquery(Device, 'cluster'), - vm_count=get_subquery(VirtualMachine, 'cluster') + device_count=count_related(Device, 'cluster'), + vm_count=count_related(VirtualMachine, 'cluster') ), 'filterset': ClusterFilterSet, 'table': ClusterTable, diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 1153b0508f3..8c959f90daa 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -12,7 +12,7 @@ from netbox.api.views import ModelViewSet from secrets import filters from secrets.exceptions import InvalidKey from secrets.models import Secret, SecretRole, SessionKey, UserKey -from utilities.utils import get_subquery +from utilities.utils import count_related from . import serializers ERR_USERKEY_MISSING = "No UserKey found for the current user." @@ -35,7 +35,7 @@ class SecretsRootView(APIRootView): class SecretRoleViewSet(ModelViewSet): queryset = SecretRole.objects.annotate( - secret_count=get_subquery(Secret, 'role') + secret_count=count_related(Secret, 'role') ) serializer_class = serializers.SecretRoleSerializer filterset_class = filters.SecretRoleFilterSet diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 7bfa265d652..3fb8d17404d 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -7,7 +7,7 @@ from django.utils.html import escape from django.utils.safestring import mark_safe from netbox.views import generic -from utilities.utils import get_subquery +from utilities.utils import count_related from . import filters, forms, tables from .models import SecretRole, Secret, SessionKey, UserKey @@ -28,7 +28,7 @@ def get_session_key(request): class SecretRoleListView(generic.ObjectListView): queryset = SecretRole.objects.annotate( - secret_count=get_subquery(Secret, 'role') + secret_count=count_related(Secret, 'role') ) table = tables.SecretRoleTable @@ -50,7 +50,7 @@ class SecretRoleBulkImportView(generic.BulkImportView): class SecretRoleBulkDeleteView(generic.BulkDeleteView): queryset = SecretRole.objects.annotate( - secret_count=get_subquery(Secret, 'role') + secret_count=count_related(Secret, 'role') ) table = tables.SecretRoleTable diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index 34be4991e21..2b7ae836567 100644 --- a/netbox/tenancy/api/views.py +++ b/netbox/tenancy/api/views.py @@ -7,7 +7,7 @@ from ipam.models import IPAddress, Prefix, VLAN, VRF from netbox.api.views import ModelViewSet from tenancy import filters from tenancy.models import Tenant, TenantGroup -from utilities.utils import get_subquery +from utilities.utils import count_related from virtualization.models import VirtualMachine from . import serializers @@ -44,15 +44,15 @@ class TenantViewSet(CustomFieldModelViewSet): queryset = Tenant.objects.prefetch_related( 'group', 'tags' ).annotate( - circuit_count=get_subquery(Circuit, 'tenant'), - device_count=get_subquery(Device, 'tenant'), - ipaddress_count=get_subquery(IPAddress, 'tenant'), - prefix_count=get_subquery(Prefix, 'tenant'), - rack_count=get_subquery(Rack, 'tenant'), - site_count=get_subquery(Site, 'tenant'), - virtualmachine_count=get_subquery(VirtualMachine, 'tenant'), - vlan_count=get_subquery(VLAN, 'tenant'), - vrf_count=get_subquery(VRF, 'tenant') + circuit_count=count_related(Circuit, 'tenant'), + device_count=count_related(Device, 'tenant'), + ipaddress_count=count_related(IPAddress, 'tenant'), + prefix_count=count_related(Prefix, 'tenant'), + rack_count=count_related(Rack, 'tenant'), + site_count=count_related(Site, 'tenant'), + virtualmachine_count=count_related(VirtualMachine, 'tenant'), + vlan_count=count_related(VLAN, 'tenant'), + vrf_count=count_related(VRF, 'tenant') ) serializer_class = serializers.TenantSerializer filterset_class = filters.TenantFilterSet diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 19e08dfd4a5..d76b469b268 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -66,7 +66,7 @@ def dynamic_import(name): return mod -def get_subquery(model, field): +def count_related(model, field): """ Return a Subquery suitable for annotating a child object count. """ diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index a3dea00df3c..ce5cb9f2c30 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -2,7 +2,7 @@ from rest_framework.routers import APIRootView from dcim.models import Device from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet, ModelViewSet -from utilities.utils import get_subquery +from utilities.utils import count_related from virtualization import filters from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from . import serializers @@ -22,7 +22,7 @@ class VirtualizationRootView(APIRootView): class ClusterTypeViewSet(ModelViewSet): queryset = ClusterType.objects.annotate( - cluster_count=get_subquery(Cluster, 'type') + cluster_count=count_related(Cluster, 'type') ) serializer_class = serializers.ClusterTypeSerializer filterset_class = filters.ClusterTypeFilterSet @@ -30,7 +30,7 @@ class ClusterTypeViewSet(ModelViewSet): class ClusterGroupViewSet(ModelViewSet): queryset = ClusterGroup.objects.annotate( - cluster_count=get_subquery(Cluster, 'group') + cluster_count=count_related(Cluster, 'group') ) serializer_class = serializers.ClusterGroupSerializer filterset_class = filters.ClusterGroupFilterSet @@ -40,8 +40,8 @@ class ClusterViewSet(CustomFieldModelViewSet): queryset = Cluster.objects.prefetch_related( 'type', 'group', 'tenant', 'site', 'tags' ).annotate( - device_count=get_subquery(Device, 'cluster'), - virtualmachine_count=get_subquery(VirtualMachine, 'cluster') + device_count=count_related(Device, 'cluster'), + virtualmachine_count=count_related(VirtualMachine, 'cluster') ) serializer_class = serializers.ClusterSerializer filterset_class = filters.ClusterFilterSet diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 05fe3267914..9ef4a086376 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -11,7 +11,7 @@ from ipam.models import IPAddress, Service from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable from netbox.views import generic from secrets.models import Secret -from utilities.utils import get_subquery +from utilities.utils import count_related from . import filters, forms, tables from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface @@ -22,7 +22,7 @@ from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterf class ClusterTypeListView(generic.ObjectListView): queryset = ClusterType.objects.annotate( - cluster_count=get_subquery(Cluster, 'type') + cluster_count=count_related(Cluster, 'type') ) table = tables.ClusterTypeTable @@ -44,7 +44,7 @@ class ClusterTypeBulkImportView(generic.BulkImportView): class ClusterTypeBulkDeleteView(generic.BulkDeleteView): queryset = ClusterType.objects.annotate( - cluster_count=get_subquery(Cluster, 'type') + cluster_count=count_related(Cluster, 'type') ) table = tables.ClusterTypeTable @@ -55,7 +55,7 @@ class ClusterTypeBulkDeleteView(generic.BulkDeleteView): class ClusterGroupListView(generic.ObjectListView): queryset = ClusterGroup.objects.annotate( - cluster_count=get_subquery(Cluster, 'group') + cluster_count=count_related(Cluster, 'group') ) table = tables.ClusterGroupTable @@ -77,7 +77,7 @@ class ClusterGroupBulkImportView(generic.BulkImportView): class ClusterGroupBulkDeleteView(generic.BulkDeleteView): queryset = ClusterGroup.objects.annotate( - cluster_count=get_subquery(Cluster, 'group') + cluster_count=count_related(Cluster, 'group') ) table = tables.ClusterGroupTable @@ -89,8 +89,8 @@ class ClusterGroupBulkDeleteView(generic.BulkDeleteView): class ClusterListView(generic.ObjectListView): permission_required = 'virtualization.view_cluster' queryset = Cluster.objects.annotate( - device_count=get_subquery(Device, 'cluster'), - vm_count=get_subquery(VirtualMachine, 'cluster') + device_count=count_related(Device, 'cluster'), + vm_count=count_related(VirtualMachine, 'cluster') ) table = tables.ClusterTable filterset = filters.ClusterFilterSet From af2777c580bb90deb17254dd42968ee8ad71fc04 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 17 Dec 2020 15:50:47 -0500 Subject: [PATCH 12/25] Fixes #5487: Support filtering rack type/width with multiple values --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/filters.py | 10 ++++++++-- netbox/dcim/tests/test_filters.py | 10 ++++------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 719a982ad65..42a6a094e0f 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -9,6 +9,7 @@ * [#5478](https://github.com/netbox-community/netbox/issues/5478) - Fix display of route target description * [#5484](https://github.com/netbox-community/netbox/issues/5484) - Fix "tagged" indication in VLAN members list * [#5486](https://github.com/netbox-community/netbox/issues/5486) - Optimize retrieval of config context data for device/VM REST API views +* [#5487](https://github.com/netbox-community/netbox/issues/5487) - Support filtering rack type/width with multiple values --- diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 3f104ef1880..f4e3fa09e68 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -224,6 +224,12 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, choices=RackStatusChoices, null_value=None ) + type = django_filters.MultipleChoiceFilter( + choices=RackTypeChoices + ) + width = django_filters.MultipleChoiceFilter( + choices=RackWidthChoices + ) role_id = django_filters.ModelMultipleChoiceFilter( queryset=RackRole.objects.all(), label='Role (ID)', @@ -242,8 +248,8 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, class Meta: model = Rack fields = [ - 'id', 'name', 'facility_id', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', - 'outer_width', 'outer_depth', 'outer_unit', + 'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'desc_units', 'outer_width', 'outer_depth', + 'outer_unit', ] def search(self, queryset, name, value): diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filters.py index f209cd1f48a..724aaf1fa62 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filters.py @@ -329,7 +329,7 @@ class RackTestCase(TestCase): racks = ( Rack(name='Rack 1', facility_id='rack-1', site=sites[0], group=rack_groups[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER), - Rack(name='Rack 2', facility_id='rack-2', site=sites[1], group=rack_groups[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_19IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER), + Rack(name='Rack 2', facility_id='rack-2', site=sites[1], group=rack_groups[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER), Rack(name='Rack 3', facility_id='rack-3', site=sites[2], group=rack_groups[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH), ) Rack.objects.bulk_create(racks) @@ -351,13 +351,11 @@ class RackTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_type(self): - # TODO: Test for multiple values - params = {'type': RackTypeChoices.TYPE_2POST} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'type': [RackTypeChoices.TYPE_2POST, RackTypeChoices.TYPE_4POST]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_width(self): - # TODO: Test for multiple values - params = {'width': RackWidthChoices.WIDTH_19IN} + params = {'width': [RackWidthChoices.WIDTH_19IN, RackWidthChoices.WIDTH_21IN]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_u_height(self): From 34a0e2e72d1dd6fc432f0b73a928f7bc80c99b8f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 17 Dec 2020 15:55:22 -0500 Subject: [PATCH 13/25] Closes #5489: Add filters for type and width to racks list --- docs/release-notes/version-2.10.md | 4 ++++ netbox/dcim/forms.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 42a6a094e0f..a7404ab3ccd 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -2,6 +2,10 @@ ## v2.10.2 (FUTURE) +### Enhancements + +* [#5489](https://github.com/netbox-community/netbox/issues/5489) - Add filters for type and width to racks list + ### Bug Fixes * [#5468](https://github.com/netbox-community/netbox/issues/5468) - Fix unlocking secrets from device/VM view diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index cb2aa10e622..930ac166bfb 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -690,6 +690,16 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): required=False, widget=StaticSelect2Multiple() ) + type = forms.MultipleChoiceField( + choices=RackTypeChoices, + required=False, + widget=StaticSelect2Multiple() + ) + width = forms.MultipleChoiceField( + choices=RackWidthChoices, + required=False, + widget=StaticSelect2Multiple() + ) role = DynamicModelMultipleChoiceField( queryset=RackRole.objects.all(), to_field_name='slug', From dac93b9840300ef96499354ddee0ca4e4a2dacaa Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Thu, 17 Dec 2020 16:11:16 -0600 Subject: [PATCH 14/25] Fixes #5254 - Require plugin authors to set zip_safe=False --- docs/plugins/development.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/plugins/development.md b/docs/plugins/development.md index d65e7d8305b..f008da2fbb3 100644 --- a/docs/plugins/development.md +++ b/docs/plugins/development.md @@ -63,11 +63,15 @@ setup( install_requires=[], packages=find_packages(), include_package_data=True, + zip_safe=False, ) ``` Many of these are self-explanatory, but for more information, see the [setuptools documentation](https://setuptools.readthedocs.io/en/latest/setuptools.html). +!!! note + `zip_safe=False` is **required** as the current plugin iteration is not zip safe due to upstream python issue [issue19699](https://bugs.python.org/issue19699) + ### Define a PluginConfig The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below: From 6ca9e2a777850ca5b9a3c0b734c5aba884f7b364 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 18 Dec 2020 12:09:41 -0500 Subject: [PATCH 15/25] Closes #5496: Add form field to filter rack reservation by user --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/forms.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index a7404ab3ccd..2534d2403f9 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -5,6 +5,7 @@ ### Enhancements * [#5489](https://github.com/netbox-community/netbox/issues/5489) - Add filters for type and width to racks list +* [#5496](https://github.com/netbox-community/netbox/issues/5496) - Add form field to filter rack reservation by user ### Bug Fixes diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 930ac166bfb..7ecd4efd804 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -21,7 +21,7 @@ from ipam.models import IPAddress, VLAN from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant, TenantGroup from utilities.forms import ( - APISelect, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, + APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, CommentField, CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, NumericArrayField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, @@ -860,7 +860,7 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditFor class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm): model = RackReservation - field_order = ['q', 'region', 'site', 'group_id', 'tenant_group', 'tenant'] + field_order = ['q', 'region', 'site', 'group_id', 'user_id', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' @@ -884,6 +884,15 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm): label='Rack group', null_option='None' ) + user_id = DynamicModelMultipleChoiceField( + queryset=User.objects.all(), + required=False, + display_field='username', + label='User', + widget=APISelectMultiple( + api_url='/api/users/users/', + ) + ) tag = TagFilterField(model) From b1402f3bae74d4e2fcf45868e5bb83751650fb77 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 18 Dec 2020 12:18:59 -0500 Subject: [PATCH 16/25] Fixes #5498: Fix filtering rack reservations by username --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/filters.py | 2 +- netbox/dcim/tests/test_filters.py | 5 ++--- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 2534d2403f9..2d5b4f39078 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -15,6 +15,7 @@ * [#5484](https://github.com/netbox-community/netbox/issues/5484) - Fix "tagged" indication in VLAN members list * [#5486](https://github.com/netbox-community/netbox/issues/5486) - Optimize retrieval of config context data for device/VM REST API views * [#5487](https://github.com/netbox-community/netbox/issues/5487) - Support filtering rack type/width with multiple values +* [#5498](https://github.com/netbox-community/netbox/issues/5498) - Fix filtering rack reservations by username --- diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index f4e3fa09e68..3046a0f3371 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -302,7 +302,7 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet): label='User (ID)', ) user = django_filters.ModelMultipleChoiceFilter( - field_name='user', + field_name='user__username', queryset=User.objects.all(), to_field_name='username', label='User (name)', diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filters.py index 724aaf1fa62..c701c47cf8e 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filters.py @@ -514,9 +514,8 @@ class RackReservationTestCase(TestCase): users = User.objects.all()[:2] params = {'user_id': [users[0].pk, users[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - # TODO: Filtering by username is broken - # params = {'user': [users[0].username, users[1].username]} - # self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'user': [users[0].username, users[1].username]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_tenant(self): tenants = Tenant.objects.all()[:2] From f2d028ba29c2994a49618cf65396d5cd9e4a538c Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Fri, 18 Dec 2020 11:33:28 -0600 Subject: [PATCH 17/25] Update version-2.10.md --- docs/release-notes/version-2.10.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 2d5b4f39078..1580b9077ca 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -9,6 +9,7 @@ ### Bug Fixes +* [#5254](https://github.com/netbox-community/netbox/issues/5254) - Require plugin authors to set zip_safe=False * [#5468](https://github.com/netbox-community/netbox/issues/5468) - Fix unlocking secrets from device/VM view * [#5473](https://github.com/netbox-community/netbox/issues/5473) - Fix alignment of rack names in elevations list * [#5478](https://github.com/netbox-community/netbox/issues/5478) - Fix display of route target description From 442dee72b59f727109ab993db5f17b64f61e649f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 18 Dec 2020 15:02:52 -0500 Subject: [PATCH 18/25] Fixes #5499: Fix filtering of displayed device/VM interfaces by regex --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/tables/devices.py | 3 ++- netbox/project-static/js/interface_filtering.js | 7 +++---- netbox/virtualization/tables.py | 3 +++ 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 1580b9077ca..ca53d0cbfe8 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -17,6 +17,7 @@ * [#5486](https://github.com/netbox-community/netbox/issues/5486) - Optimize retrieval of config context data for device/VM REST API views * [#5487](https://github.com/netbox-community/netbox/issues/5487) - Support filtering rack type/width with multiple values * [#5498](https://github.com/netbox-community/netbox/issues/5498) - Fix filtering rack reservations by username +* [#5499](https://github.com/netbox-community/netbox/issues/5499) - Fix filtering of displayed device/VM interfaces by regex --- diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 536be66d93f..66320650519 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -447,7 +447,8 @@ class DeviceInterfaceTable(InterfaceTable): 'connection', 'actions', ) row_attrs = { - 'class': lambda record: record.cable.get_status_class() if record.cable else '' + 'class': lambda record: record.cable.get_status_class() if record.cable else '', + 'data-name': lambda record: record.name, } diff --git a/netbox/project-static/js/interface_filtering.js b/netbox/project-static/js/interface_filtering.js index fecb156f4bb..51ac701982a 100644 --- a/netbox/project-static/js/interface_filtering.js +++ b/netbox/project-static/js/interface_filtering.js @@ -1,11 +1,10 @@ // Inteface filtering $('input.interface-filter').on('input', function() { - var filter = new RegExp(this.value); - var interface; + let filter = new RegExp(this.value); + let interface; for (interface of $('table > tbody > tr')) { - // Slice off 'interface_' at the start of the ID - if (filter.test(interface.id.slice(10))) { + if (filter.test(interface.getAttribute('data-name'))) { // Match the toggle in case the filter now matches the interface $(interface).find('input:checkbox[name=pk]').prop('checked', $('input.toggle').prop('checked')); $(interface).show(); diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index 97e1d6e36b6..34a07062349 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -183,3 +183,6 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable): default_columns = ( 'pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses', 'actions', ) + row_attrs = { + 'data-name': lambda record: record.name, + } From 1e5778e6db1b08dc052c9abc1ad7e6dfe3fb9708 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 18 Dec 2020 15:43:15 -0500 Subject: [PATCH 19/25] Clean up prefix hierarchy annotation --- netbox/ipam/tables.py | 12 +++++------- netbox/utilities/templatetags/helpers.py | 8 ++++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 868ba310555..bea8ec25523 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -18,13 +18,11 @@ UTILIZATION_GRAPH = """ """ PREFIX_LINK = """ -{% if record.children %} - -{% else %} - -{% endif %} - {{ record.prefix }} - +{% load helpers %} +{% for i in record.parents|as_range %} + +{% endfor %} +{{ record.prefix }} """ PREFIX_ROLE_LINK = """ diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index f095af58fc1..a93abe1a5f9 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -208,6 +208,14 @@ def split(string, sep=','): return string.split(sep) +@register.filter() +def as_range(n): + """ + Return a range of n items. + """ + return range(n) + + # # Tags # From 158ed5765bd04633c273452aa06c1f77264cb56f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 21 Dec 2020 09:43:15 -0500 Subject: [PATCH 20/25] Fixes #5507: Fix custom field data assignment via UI for IP addresses, secrets --- docs/release-notes/version-2.10.md | 1 + netbox/ipam/forms.py | 1 + netbox/secrets/forms.py | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index ca53d0cbfe8..4c643a23aec 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -18,6 +18,7 @@ * [#5487](https://github.com/netbox-community/netbox/issues/5487) - Support filtering rack type/width with multiple values * [#5498](https://github.com/netbox-community/netbox/issues/5498) - Fix filtering rack reservations by username * [#5499](https://github.com/netbox-community/netbox/issues/5499) - Fix filtering of displayed device/VM interfaces by regex +* [#5507](https://github.com/netbox-community/netbox/issues/5507) - Fix custom field data assignment via UI for IP addresses, secrets --- diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 29a6d295ea9..e2cb514172e 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -774,6 +774,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel self.initial['primary_for_parent'] = True def clean(self): + super().clean() # Cannot select both a device interface and a VM interface if self.cleaned_data.get('interface') and self.cleaned_data.get('vminterface'): diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 8e976c8ead8..cdd843e2d72 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -122,6 +122,7 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm): self.fields['plaintext'].required = True def clean(self): + super().clean() if not self.cleaned_data['device'] and not self.cleaned_data['virtual_machine']: raise forms.ValidationError("Secrets must be assigned to a device or virtual machine.") From b09112941a8120a08b17992854daad957f2c1ecc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 21 Dec 2020 11:47:11 -0500 Subject: [PATCH 21/25] Fixes #5510: Fix filtering by boolean custom fields --- docs/release-notes/version-2.10.md | 1 + netbox/extras/filters.py | 28 +++---- netbox/extras/tests/test_customfields.py | 100 +++++++++++++++++++++++ 3 files changed, 114 insertions(+), 15 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 4c643a23aec..daa5c0eaecc 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -19,6 +19,7 @@ * [#5498](https://github.com/netbox-community/netbox/issues/5498) - Fix filtering rack reservations by username * [#5499](https://github.com/netbox-community/netbox/issues/5499) - Fix filtering of displayed device/VM interfaces by regex * [#5507](https://github.com/netbox-community/netbox/issues/5507) - Fix custom field data assignment via UI for IP addresses, secrets +* [#5510](https://github.com/netbox-community/netbox/issues/5510) - Fix filtering by boolean custom fields --- diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index 7b341f74dcb..e3c313735e9 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -2,6 +2,7 @@ import django_filters from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.db.models import Q +from django.forms import DateField, IntegerField, NullBooleanField from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup @@ -38,24 +39,21 @@ class CustomFieldFilter(django_filters.Filter): """ def __init__(self, custom_field, *args, **kwargs): self.custom_field = custom_field + + if custom_field.type == CustomFieldTypeChoices.TYPE_INTEGER: + self.field_class = IntegerField + elif custom_field.type == CustomFieldTypeChoices.TYPE_BOOLEAN: + self.field_class = NullBooleanField + elif custom_field.type == CustomFieldTypeChoices.TYPE_DATE: + self.field_class = DateField + super().__init__(*args, **kwargs) - def filter(self, queryset, value): + self.field_name = f'custom_field_data__{self.field_name}' - # Skip filter on empty value - if value is None or not value.strip(): - return queryset - - # Apply the assigned filter logic (exact or loose) - if ( - self.custom_field.type in EXACT_FILTER_TYPES or - self.custom_field.filter_logic == CustomFieldFilterLogicChoices.FILTER_EXACT - ): - kwargs = {f'custom_field_data__{self.field_name}': value} - else: - kwargs = {f'custom_field_data__{self.field_name}__icontains': value} - - return queryset.filter(**kwargs) + if custom_field.type not in EXACT_FILTER_TYPES: + if custom_field.filter_logic == CustomFieldFilterLogicChoices.FILTER_LOOSE: + self.lookup_expr = 'icontains' class CustomFieldModelFilterSet(django_filters.FilterSet): diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index fe56027dc14..4f7a676767c 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError from django.urls import reverse from rest_framework import status +from dcim.filters import SiteFilterSet from dcim.forms import SiteCSVForm from dcim.models import Site, Rack from extras.choices import * @@ -597,3 +598,102 @@ class CustomFieldModelTest(TestCase): site.cf['baz'] = 'def' site.clean() + + +class CustomFieldFilterTest(TestCase): + queryset = Site.objects.all() + filterset = SiteFilterSet + + @classmethod + def setUpTestData(cls): + obj_type = ContentType.objects.get_for_model(Site) + + # Integer filtering + cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER) + cf.save() + cf.content_types.set([obj_type]) + + # Boolean filtering + cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_BOOLEAN) + cf.save() + cf.content_types.set([obj_type]) + + # Exact text filtering + cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_TEXT, + filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT) + cf.save() + cf.content_types.set([obj_type]) + + # Loose text filtering + cf = CustomField(name='cf4', type=CustomFieldTypeChoices.TYPE_TEXT, + filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE) + cf.save() + cf.content_types.set([obj_type]) + + # Date filtering + cf = CustomField(name='cf5', type=CustomFieldTypeChoices.TYPE_DATE) + cf.save() + cf.content_types.set([obj_type]) + + # Exact URL filtering + cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_URL, + filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT) + cf.save() + cf.content_types.set([obj_type]) + + # Loose URL filtering + cf = CustomField(name='cf7', type=CustomFieldTypeChoices.TYPE_URL, + filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE) + cf.save() + cf.content_types.set([obj_type]) + + # Selection filtering + cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_URL, choices=['Foo', 'Bar', 'Baz']) + cf.save() + cf.content_types.set([obj_type]) + + Site.objects.bulk_create([ + Site(name='Site 1', slug='site-1', custom_field_data={ + 'cf1': 100, + 'cf2': True, + 'cf3': 'foo', + 'cf4': 'foo', + 'cf5': '2016-06-26', + 'cf6': 'http://foo.example.com/', + 'cf7': 'http://foo.example.com/', + 'cf8': 'Foo', + }), + Site(name='Site 2', slug='site-2', custom_field_data={ + 'cf1': 200, + 'cf2': False, + 'cf3': 'foobar', + 'cf4': 'foobar', + 'cf5': '2016-06-27', + 'cf6': 'http://bar.example.com/', + 'cf7': 'http://bar.example.com/', + 'cf8': 'Bar', + }), + Site(name='Site 3', slug='site-3', custom_field_data={ + }), + ]) + + def test_filter_integer(self): + self.assertEqual(self.filterset({'cf_cf1': 100}, self.queryset).qs.count(), 1) + + def test_filter_boolean(self): + self.assertEqual(self.filterset({'cf_cf2': True}, self.queryset).qs.count(), 1) + self.assertEqual(self.filterset({'cf_cf2': False}, self.queryset).qs.count(), 1) + + def test_filter_text(self): + self.assertEqual(self.filterset({'cf_cf3': 'foo'}, self.queryset).qs.count(), 1) + self.assertEqual(self.filterset({'cf_cf4': 'foo'}, self.queryset).qs.count(), 2) + + def test_filter_date(self): + self.assertEqual(self.filterset({'cf_cf5': '2016-06-26'}, self.queryset).qs.count(), 1) + + def test_filter_url(self): + self.assertEqual(self.filterset({'cf_cf6': 'http://foo.example.com/'}, self.queryset).qs.count(), 1) + self.assertEqual(self.filterset({'cf_cf7': 'example.com'}, self.queryset).qs.count(), 2) + + def test_filter_select(self): + self.assertEqual(self.filterset({'cf_cf8': 'Foo'}, self.queryset).qs.count(), 1) From 278c88105942e4505c0596812101a6a7491f7749 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 21 Dec 2020 11:54:35 -0500 Subject: [PATCH 22/25] as_range: Catch TypeErrors --- netbox/utilities/templatetags/helpers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index a93abe1a5f9..29c920d4f51 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -213,6 +213,10 @@ def as_range(n): """ Return a range of n items. """ + try: + int(n) + except TypeError: + return list() return range(n) From c5a265e8286daf119de48d6f10b3ffbca8276e9c Mon Sep 17 00:00:00 2001 From: Robin Schneider Date: Mon, 21 Dec 2020 18:02:30 +0100 Subject: [PATCH 23/25] Use HTTPS URL schema everywhere (#5505) * Use HTTPS everywhere (mechanical edit using util from https-everywhere) ```Shell node ~/src/EFForg/https-everywhere/utils/rewriter/rewriter.js . git checkout netbox/project-static/ ``` A few additional changes where reset manually before the commit. * Use HTTPS everywhere (mechanical edit using util from opening_hours.js) ```Shell make -f ~/src/opening-hours/opening_hours.js/Makefile qa-https-everywhere git checkout netbox/project-static/ git checkout netbox/*/tests ``` --- README.md | 6 +++--- docs/configuration/optional-settings.md | 4 ++-- docs/installation/5-http-server.md | 2 +- docs/installation/6-ldap.md | 2 +- docs/release-notes/version-2.1.md | 2 +- docs/release-notes/version-2.2.md | 2 +- docs/rest-api/overview.md | 2 +- netbox/netbox/configuration.example.py | 4 ++-- netbox/templates/base.html | 2 +- netbox/templates/dcim/site.html | 4 ++-- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 0b9531df05d..68927463d63 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ to address the needs of network and infrastructure engineers. It is intended to function as a domain-specific source of truth for network operations. NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) -Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a +Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox). -The complete documentation for NetBox can be found at [Read the Docs](http://netbox.readthedocs.io/en/stable/). +The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). Questions? Comments? Please start a [discussion on GitHub](https://github.com/netbox-community/netbox/discussions), or join us in the **#netbox** Slack channel on [NetworkToCode](https://networktocode.slack.com/)! @@ -36,7 +36,7 @@ or join us in the **#netbox** Slack channel on [NetworkToCode](https://networkto ## Installation -Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for +Please see [the documentation](https://netbox.readthedocs.io/en/stable/) for instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/netbox-community/netbox/releases) and run `upgrade.sh`. diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index fe43f0483b7..91c0e759776 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -44,7 +44,7 @@ This defines custom content to be displayed on the login page above the login fo Default: None -The base URL path to use when accessing NetBox. Do not include the scheme or domain name. For example, if installed at http://example.com/netbox/, set: +The base URL path to use when accessing NetBox. Do not include the scheme or domain name. For example, if installed at https://example.com/netbox/, set: ```python BASE_PATH = 'netbox/' @@ -318,7 +318,7 @@ NetBox will use these credentials when authenticating to remote devices via the ## NAPALM_ARGS -A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](http://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example: +A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](https://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example: ```python NAPALM_ARGS = { diff --git a/docs/installation/5-http-server.md b/docs/installation/5-http-server.md index eba0db21bbc..90796455494 100644 --- a/docs/installation/5-http-server.md +++ b/docs/installation/5-http-server.md @@ -1,6 +1,6 @@ # HTTP Server Setup -This documentation provides example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/current/), though any HTTP server which supports WSGI should be compatible. +This documentation provides example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](https://httpd.apache.org/docs/current/), though any HTTP server which supports WSGI should be compatible. !!! info For the sake of brevity, only Ubuntu 20.04 instructions are provided here. These tasks are not unique to NetBox and should carry over to other distributions with minimal changes. Please consult your distribution's documentation for assistance if needed. diff --git a/docs/installation/6-ldap.md b/docs/installation/6-ldap.md index ce626253183..25f9c8f2b30 100644 --- a/docs/installation/6-ldap.md +++ b/docs/installation/6-ldap.md @@ -41,7 +41,7 @@ First, enable the LDAP authentication backend in `configuration.py`. (Be sure to REMOTE_AUTH_BACKEND = 'netbox.authentication.LDAPBackend' ``` -Next, create a file in the same directory as `configuration.py` (typically `/opt/netbox/netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`. Complete documentation of all `django-auth-ldap` configuration options is included in the project's [official documentation](http://django-auth-ldap.readthedocs.io/). +Next, create a file in the same directory as `configuration.py` (typically `/opt/netbox/netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`. Complete documentation of all `django-auth-ldap` configuration options is included in the project's [official documentation](https://django-auth-ldap.readthedocs.io/). ### General Server Configuration diff --git a/docs/release-notes/version-2.1.md b/docs/release-notes/version-2.1.md index 59f23c0904e..e5fa41d8211 100644 --- a/docs/release-notes/version-2.1.md +++ b/docs/release-notes/version-2.1.md @@ -121,7 +121,7 @@ A new API endpoint has been added at `/api/ipam/prefixes//available-ips/`. A #### NAPALM Integration ([#1348](https://github.com/netbox-community/netbox/issues/1348)) -The [NAPALM automation](https://napalm-automation.net/) library provides an abstracted interface for pulling live data (e.g. uptime, software version, running config, LLDP neighbors, etc.) from network devices. The NetBox API has been extended to support executing read-only NAPALM methods on devices defined in NetBox. To enable this functionality, ensure that NAPALM has been installed (`pip install napalm`) and the `NETBOX_USERNAME` and `NETBOX_PASSWORD` [configuration parameters](http://netbox.readthedocs.io/en/stable/configuration/optional-settings/#netbox_username) have been set in configuration.py. +The [NAPALM automation](https://napalm-automation.net/) library provides an abstracted interface for pulling live data (e.g. uptime, software version, running config, LLDP neighbors, etc.) from network devices. The NetBox API has been extended to support executing read-only NAPALM methods on devices defined in NetBox. To enable this functionality, ensure that NAPALM has been installed (`pip install napalm`) and the `NETBOX_USERNAME` and `NETBOX_PASSWORD` [configuration parameters](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/#netbox_username) have been set in configuration.py. ### Enhancements diff --git a/docs/release-notes/version-2.2.md b/docs/release-notes/version-2.2.md index 905b7a8d109..e13c4fe6906 100644 --- a/docs/release-notes/version-2.2.md +++ b/docs/release-notes/version-2.2.md @@ -196,7 +196,7 @@ Our second-most popular feature request has arrived! NetBox now supports the cre #### Custom Validation Reports ([#1511](https://github.com/netbox-community/netbox/issues/1511)) -Users can now create custom reports which are run to validate data in NetBox. Reports work very similar to Python unit tests: Each report inherits from NetBox's Report class and contains one or more test method. Reports can be run and retrieved via the web UI, API, or CLI. See [the docs](http://netbox.readthedocs.io/en/stable/miscellaneous/reports/) for more info. +Users can now create custom reports which are run to validate data in NetBox. Reports work very similar to Python unit tests: Each report inherits from NetBox's Report class and contains one or more test method. Reports can be run and retrieved via the web UI, API, or CLI. See [the docs](https://netbox.readthedocs.io/en/stable/miscellaneous/reports/) for more info. ### Enhancements diff --git a/docs/rest-api/overview.md b/docs/rest-api/overview.md index a3c8143ebec..290343aa680 100644 --- a/docs/rest-api/overview.md +++ b/docs/rest-api/overview.md @@ -2,7 +2,7 @@ ## What is a REST API? -REST stands for [representational state transfer](https://en.wikipedia.org/wiki/Representational_state_transfer). It's a particular type of API which employs HTTP requests and [JavaScript Object Notation (JSON)](http://www.json.org/) to facilitate create, retrieve, update, and delete (CRUD) operations on objects within an application. Each type of operation is associated with a particular HTTP verb: +REST stands for [representational state transfer](https://en.wikipedia.org/wiki/Representational_state_transfer). It's a particular type of API which employs HTTP requests and [JavaScript Object Notation (JSON)](https://www.json.org/) to facilitate create, retrieve, update, and delete (CRUD) operations on objects within an application. Each type of operation is associated with a particular HTTP verb: * `GET`: Retrieve an object or list of objects * `POST`: Create an object diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 51c73bccc01..b7a72a5043f 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -79,7 +79,7 @@ BANNER_BOTTOM = '' # Text to include on the login page above the login form. HTML is allowed. BANNER_LOGIN = '' -# Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set: +# Base URL path if accessing NetBox within a directory. For example, if installed at https://example.com/netbox/, set: # BASE_PATH = 'netbox/' BASE_PATH = '' @@ -183,7 +183,7 @@ NAPALM_PASSWORD = '' # NAPALM timeout (in seconds). (Default: 30) NAPALM_TIMEOUT = 30 -# NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must +# NAPALM optional arguments (see https://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must # be provided as a dictionary. NAPALM_ARGS = {} diff --git a/netbox/templates/base.html b/netbox/templates/base.html index 86b582b3ef0..f3129d7dd65 100644 --- a/netbox/templates/base.html +++ b/netbox/templates/base.html @@ -71,7 +71,7 @@

- Docs · + Docs · API · Code · Help diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index a2479ca1f78..a0e713fcff6 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -137,7 +137,7 @@

{% if object.physical_address %} @@ -156,7 +156,7 @@ {% if object.latitude and object.longitude %} From 23f334e5dfadfc7e8e1b1c0e28076bc033298a15 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 21 Dec 2020 15:30:34 -0500 Subject: [PATCH 24/25] Fixes #5488: Fix caching error when viewing cable trace after toggling cable status --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/signals.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index daa5c0eaecc..00ac081d7f6 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -16,6 +16,7 @@ * [#5484](https://github.com/netbox-community/netbox/issues/5484) - Fix "tagged" indication in VLAN members list * [#5486](https://github.com/netbox-community/netbox/issues/5486) - Optimize retrieval of config context data for device/VM REST API views * [#5487](https://github.com/netbox-community/netbox/issues/5487) - Support filtering rack type/width with multiple values +* [#5488](https://github.com/netbox-community/netbox/issues/5488) - Fix caching error when viewing cable trace after toggling cable status * [#5498](https://github.com/netbox-community/netbox/issues/5498) - Fix filtering rack reservations by username * [#5499](https://github.com/netbox-community/netbox/issues/5499) - Fix filtering of displayed device/VM interfaces by regex * [#5507](https://github.com/netbox-community/netbox/issues/5507) - Fix custom field data assignment via UI for IP addresses, secrets diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 4a5340748d1..33c4b461c50 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -1,5 +1,6 @@ import logging +from cacheops import invalidate_obj from django.contrib.contenttypes.models import ContentType from django.db.models.signals import post_save, post_delete, pre_delete from django.db import transaction @@ -30,6 +31,7 @@ def rebuild_paths(obj): with transaction.atomic(): for cp in cable_paths: + invalidate_obj(cp.origin) cp.delete() create_cablepath(cp.origin) From e196d82e767ebeb015e1b0da403593efac3603e5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 21 Dec 2020 16:03:43 -0500 Subject: [PATCH 25/25] Release v2.10.2 --- docs/release-notes/version-2.10.md | 2 +- netbox/netbox/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 00ac081d7f6..27965090bbd 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -1,6 +1,6 @@ # NetBox v2.10 -## v2.10.2 (FUTURE) +## v2.10.2 (2020-12-21) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index d496f9969f7..3a6dc473fed 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ from django.core.validators import URLValidator # Environment setup # -VERSION = '2.10.2-dev' +VERSION = '2.10.2' # Hostname HOSTNAME = platform.node()