From 75cdb21c5d075938a43ca9ce557fa5c40389d757 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 b2269ca0e..d496f9969 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 6270339c61dc50e42e6f760ec318c4d4f99ac6bd 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 7cf199e21..03e2858eb 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 1e0813e5c..6a00308f3 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 1f4782847..a42610e35 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 e86782ffcb3f675310eff3cf4da88f1da3628788 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 03e2858eb..5c4f9f83b 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 4fa8f4424..3443d0bf4 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 d84f0c234a3ab39042523dc93ab4d21f409e6683 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 dbdbb5343..c0732649b 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 dcaf37a46c5833a9073cdbd6b2f36e9377158b41 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 d154f5017..9182457a0 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 84024dfcdeca2cd4acaa8c854461104184946700 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 5c4f9f83b..d43f57ac5 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 2ff3e4ea2..594ab43f3 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 0f4a0416d..8baec6956 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -317,5 +317,6 @@ {% block javascript %} + {% endblock %} From d087ac750f9a7c6b285f2eed31a8b17f965d9e6d 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 d43f57ac5..9fe23d253 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 02196198c..868ba3105 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 d84b55c2a0a47abc154bf49fc4489641b1804907 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 b7bbc9842..1e0611f06 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 bb69f9aee9ebf8e16dca26e40928b4a7f1ff4050 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 9fe23d253..719a982ad 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 db36c3176..f5149b876 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 e2d3d5ea5..82952ad9a 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 cfbe7ec948b111084f58c6eee6d0747b59aec3d8 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 ef5a944e2..ad497ee5f 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 f5149b876..efb8e994d 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 fcd9add7c..38077c89a 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 fb38edf46..9d09bbe03 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 617da5c6e..1153b0508 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 142203b58..34be4991e 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 52a951555..19e08dfd4 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 82952ad9a..a3dea00df 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 68eafb180abe2317285efc386df99618108b065e 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 ad497ee5f..6968da61e 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 a237b8805..9fea26652 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 efb8e994d..f9e8027b4 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 9f8e4c13f..b092be612 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 38077c89a..8ab7b0eea 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 99295de1a..57483345c 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 9d09bbe03..d9eae69aa 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 1cbac27f6..36c225045 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 a074bde4e..4c6e3103a 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 1153b0508..8c959f90d 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 7bfa265d6..3fb8d1740 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 34be4991e..2b7ae8365 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 19e08dfd4..d76b469b2 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 a3dea00df..ce5cb9f2c 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 05fe32679..9ef4a0863 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 d49edd40a9cdc985a031586d157e16430726b26d 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 719a982ad..42a6a094e 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 3f104ef18..f4e3fa09e 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 f209cd1f4..724aaf1fa 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 6425aaa6637046736352ba40e7dbb80476836d7e 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 42a6a094e..a7404ab3c 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 cb2aa10e6..930ac166b 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 1604990fa6ebe3ecdd6eefaff20574c44bb6a5ad 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 d65e7d830..f008da2fb 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 56fae2c10c34e7f9594cb73793c83bf38b976cf2 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 a7404ab3c..2534d2403 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 930ac166b..7ecd4efd8 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 197e18c39a1deeff766ff119982ddd3704e6060d 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 2534d2403..2d5b4f390 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 f4e3fa09e..3046a0f33 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 724aaf1fa..c701c47cf 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 ef472e7c0c461996491b6fc8c4cc1337406f7dfa 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 2d5b4f390..1580b9077 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 95c94b035cf7ff151576844035e55cb6d2b6a18f 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 1580b9077..ca53d0cbf 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 536be66d9..663206505 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 fecb156f4..51ac70198 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 97e1d6e36..34a070623 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 901bfc6472685843229f1f1a7b0894ed33403a96 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 868ba3105..bea8ec255 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 f095af58f..a93abe1a5 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 24ae6c156a221d8cbdc0cc637928abd196228cff 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 ca53d0cbf..4c643a23a 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 29a6d295e..e2cb51417 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 8e976c8ea..cdd843e2d 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 3e90dc91545e6f3a803702483abcce8308c06a14 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 4c643a23a..daa5c0eae 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 7b341f74d..e3c313735 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 fe56027dc..4f7a67676 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 e2a74c14781cfd87a912f149760fb851e7889eeb 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 a93abe1a5..29c920d4f 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 bb94ac3e025547cd34e402b9883c1d546e03553b 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 0b9531df0..68927463d 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 fe43f0483..91c0e7597 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 eba0db21b..907964554 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 ce6262531..25f9c8f2b 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 59f23c090..e5fa41d82 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 905b7a8d1..e13c4fe69 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 a3c8143eb..290343aa6 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 51c73bccc..b7a72a504 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 86b582b3e..f3129d7dd 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 a2479ca1f..a0e713fcf 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 de504a0f21758dea355b360b46b9a6ae697f77f7 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 daa5c0eae..00ac081d7 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 4a5340748..33c4b461c 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 2e1a675ac4e7d265e4438063ffa930ea6753b927 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 00ac081d7..27965090b 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 d496f9969..3a6dc473f 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()