diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 0c01d6eb9..54f875975 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -6,7 +6,7 @@ from dcim.views import PathTraceView from netbox.views import generic from tenancy.views import ObjectContactsView from utilities.forms import ConfirmationForm -from utilities.utils import count_related +from utilities.query import count_related from utilities.views import register_model_view from . import filtersets, forms, tables from .models import * diff --git a/netbox/core/views.py b/netbox/core/views.py index 0379900e1..400b421d5 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -25,7 +25,7 @@ from netbox.views import generic from netbox.views.generic.base import BaseObjectView from netbox.views.generic.mixins import TableMixin from utilities.forms import ConfirmationForm -from utilities.utils import count_related +from utilities.query import count_related from utilities.views import ContentTypePermissionRequiredMixin, register_model_view from . import filtersets, forms, tables from .models import * diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 49bbe9be1..120bbcb59 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -25,8 +25,8 @@ from tenancy.views import ObjectContactsView from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.permissions import get_permission_for_model +from utilities.query import count_related from utilities.query_functions import CollateAsChar -from utilities.utils import count_related from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view from virtualization.models import VirtualMachine from . import filtersets, forms, tables diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 140610ac4..2468e9236 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -21,11 +21,11 @@ from netbox.views.generic.mixins import TableMixin from utilities.data import shallow_compare_dict from utilities.forms import ConfirmationForm, get_field_value from utilities.paginator import EnhancedPaginator, get_paginate_count +from utilities.query import count_related from utilities.querydict import normalize_querydict from utilities.request import copy_safe_request from utilities.rqworker import get_workers_for_queue from utilities.templatetags.builtins.filters import render_markdown -from utilities.utils import count_related from utilities.views import ContentTypePermissionRequiredMixin, get_viewname, register_model_view from . import filtersets, forms, tables from .models import * diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py index 2ff8a8b6e..a3f37fe3c 100644 --- a/netbox/ipam/querysets.py +++ b/netbox/ipam/querysets.py @@ -3,8 +3,8 @@ from django.db.models import Count, F, OuterRef, Q, Subquery, Value from django.db.models.expressions import RawSQL from django.db.models.functions import Round +from utilities.query import count_related from utilities.querysets import RestrictedQuerySet -from utilities.utils import count_related __all__ = ( 'ASNRangeQuerySet', diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 6870d1e9e..24d82d186 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -9,8 +9,8 @@ from circuits.models import Provider from dcim.filtersets import InterfaceFilterSet from dcim.models import Interface, Site from netbox.views import generic +from utilities.query import count_related from utilities.tables import get_table_ordering -from utilities.utils import count_related from utilities.views import ViewTab, register_model_view from virtualization.filtersets import VMInterfaceFilterSet from virtualization.models import VMInterface diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index d30793a16..367bd5bc2 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -3,7 +3,8 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ from netbox.views import generic -from utilities.utils import count_related, get_related_models +from utilities.query import count_related +from utilities.utils import get_related_models from utilities.views import register_model_view, ViewTab from . import filtersets, forms, tables from .models import * diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 685f3f2bd..ab5823bf2 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -11,8 +11,9 @@ from rest_framework.views import get_view_name as drf_get_view_name from extras.constants import HTTP_CONTENT_TYPE_JSON from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound from netbox.api.fields import RelatedObjectCountField +from .query import count_related, dict_to_filter_params from .string import title -from .utils import count_related, dict_to_filter_params, dynamic_import +from .utils import dynamic_import __all__ = ( 'get_annotations_for_serializer', diff --git a/netbox/utilities/query.py b/netbox/utilities/query.py new file mode 100644 index 000000000..3a355ab67 --- /dev/null +++ b/netbox/utilities/query.py @@ -0,0 +1,56 @@ +from django.db.models import Count, OuterRef, Subquery +from django.db.models.functions import Coalesce + +__all__ = ( + 'count_related', + 'dict_to_filter_params', +) + + +def count_related(model, field): + """ + Return a Subquery suitable for annotating a child object count. + """ + subquery = Subquery( + model.objects.filter( + **{field: OuterRef('pk')} + ).order_by().values( + field + ).annotate( + c=Count('*') + ).values('c') + ) + + return Coalesce(subquery, 0) + + +def dict_to_filter_params(d, prefix=''): + """ + Translate a dictionary of attributes to a nested set of parameters suitable for QuerySet filtering. For example: + + { + "name": "Foo", + "rack": { + "facility_id": "R101" + } + } + + Becomes: + + { + "name": "Foo", + "rack__facility_id": "R101" + } + + And can be employed as filter parameters: + + Device.objects.filter(**dict_to_filter(attrs_dict)) + """ + params = {} + for key, val in d.items(): + k = prefix + key + if isinstance(val, dict): + params.update(dict_to_filter_params(val, k + '__')) + else: + params[k] = val + return params diff --git a/netbox/utilities/tests/test_utils.py b/netbox/utilities/tests/test_utils.py index 289fa3a3a..3943a3044 100644 --- a/netbox/utilities/tests/test_utils.py +++ b/netbox/utilities/tests/test_utils.py @@ -2,8 +2,8 @@ from django.http import QueryDict from django.test import TestCase from utilities.data import deepmerge +from utilities.query import dict_to_filter_params from utilities.querydict import normalize_querydict -from utilities.utils import dict_to_filter_params class DictToFilterParamsTest(TestCase): diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 7a22881f6..2e69bfa6a 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -1,5 +1,4 @@ -from django.db.models import Count, ManyToOneRel, OuterRef, Subquery -from django.db.models.functions import Coalesce +from django.db.models import ManyToOneRel from django.utils import timezone from django.utils.timezone import localtime @@ -17,55 +16,6 @@ def dynamic_import(name): return mod -def count_related(model, field): - """ - Return a Subquery suitable for annotating a child object count. - """ - subquery = Subquery( - model.objects.filter( - **{field: OuterRef('pk')} - ).order_by().values( - field - ).annotate( - c=Count('*') - ).values('c') - ) - - return Coalesce(subquery, 0) - - -def dict_to_filter_params(d, prefix=''): - """ - Translate a dictionary of attributes to a nested set of parameters suitable for QuerySet filtering. For example: - - { - "name": "Foo", - "rack": { - "facility_id": "R101" - } - } - - Becomes: - - { - "name": "Foo", - "rack__facility_id": "R101" - } - - And can be employed as filter parameters: - - Device.objects.filter(**dict_to_filter(attrs_dict)) - """ - params = {} - for key, val in d.items(): - k = prefix + key - if isinstance(val, dict): - params.update(dict_to_filter_params(val, k + '__')) - else: - params[k] = val - return params - - def content_type_name(ct, include_app=True): """ Return a human-friendly ContentType name (e.g. "DCIM > Site"). diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 6019fc227..7300635ac 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -18,8 +18,8 @@ from ipam.tables import InterfaceVLANTable from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.views import generic from tenancy.views import ObjectContactsView +from utilities.query import count_related from utilities.query_functions import CollateAsChar -from utilities.utils import count_related from utilities.views import ViewTab, register_model_view from . import filtersets, forms, tables from .models import * diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py index af1f653c8..b2dcf4038 100644 --- a/netbox/vpn/views.py +++ b/netbox/vpn/views.py @@ -1,7 +1,7 @@ from ipam.tables import RouteTargetTable from netbox.views import generic from tenancy.views import ObjectContactsView -from utilities.utils import count_related +from utilities.query import count_related from utilities.views import register_model_view from . import filtersets, forms, tables from .models import * diff --git a/netbox/wireless/views.py b/netbox/wireless/views.py index e1eb6fd7d..891bb6f84 100644 --- a/netbox/wireless/views.py +++ b/netbox/wireless/views.py @@ -1,6 +1,6 @@ from dcim.models import Interface from netbox.views import generic -from utilities.utils import count_related +from utilities.query import count_related from utilities.views import register_model_view from . import filtersets, forms, tables from .models import *