Coverage for utilities/query.py: 95%

27 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1from django.db.models import Count, OuterRef, QuerySet, Subquery 

2from django.db.models.functions import Coalesce 

3 

4__all__ = ( 

5 'count_related', 

6 'dict_to_filter_params', 

7 'reapply_model_ordering', 

8) 

9 

10 

11def count_related(model, field): 

12 """ 

13 Return a Subquery suitable for annotating a child object count. 

14 """ 

15 subquery = Subquery( 

16 model.objects.filter( 

17 **{field: OuterRef('pk')} 

18 ).order_by().values( 

19 field 

20 ).annotate( 

21 c=Count('*') 

22 ).values('c') 

23 ) 

24 

25 return Coalesce(subquery, 0) 

26 

27 

28def dict_to_filter_params(d, prefix=''): 

29 """ 

30 Translate a dictionary of attributes to a nested set of parameters suitable for QuerySet filtering. For example: 

31 

32 { 

33 "name": "Foo", 

34 "rack": { 

35 "facility_id": "R101" 

36 } 

37 } 

38 

39 Becomes: 

40 

41 { 

42 "name": "Foo", 

43 "rack__facility_id": "R101" 

44 } 

45 

46 And can be employed as filter parameters: 

47 

48 Device.objects.filter(**dict_to_filter(attrs_dict)) 

49 """ 

50 params = {} 

51 for key, val in d.items(): 

52 k = prefix + key 

53 if isinstance(val, dict): 

54 params.update(dict_to_filter_params(val, k + '__')) 

55 else: 

56 params[k] = val 

57 return params 

58 

59 

60# TODO: Remove in NetBox v5.0. MPTT support is retained only for plugins that have 

61# not yet migrated their tree models to netbox.models.ltree.LtreeModel; NetBox core 

62# no longer uses django-mptt. When MPTT support is dropped, delete this helper and 

63# its call in reapply_model_ordering(). 

64def _is_mptt_model(model) -> bool: 

65 """ 

66 Whether `model` is backed by django-mptt (deprecated). MPTT applies its own 

67 tree ordering via the TreeManager, so such querysets must NOT have plain 

68 model-level ordering reapplied after .annotate(). 

69 """ 

70 from mptt.managers import TreeManager 

71 

72 return any(isinstance(manager, TreeManager) for manager in model._meta.local_managers) 

73 

74 

75def reapply_model_ordering(queryset: QuerySet) -> QuerySet: 

76 """ 

77 Reapply model-level ordering in case it has been lost through .annotate(). 

78 https://code.djangoproject.com/ticket/32811 

79 """ 

80 # Models ordered by a trigger-maintained ltree column (`sort_path`/`path`) are 

81 # exempt. Key the check on the ordering itself, NOT on LtreeManager presence: 

82 # InventoryItem/InventoryItemTemplate use an LtreeManager only for path 

83 # maintenance but order by a regular column (name), so they DO need their 

84 # ordering reapplied after .annotate() strips it (Django #32811). 

85 ordering = queryset.model._meta.ordering or () 

86 if any(isinstance(f, str) and f.lstrip('-') in ('sort_path', 'path') for f in ordering): 

87 return queryset 

88 

89 # TODO: Remove in NetBox v5.0 (see _is_mptt_model). Plugins may still use MPTT 

90 # via the generic bulk views, so keep exempting MPTT-based models for now. 

91 if _is_mptt_model(queryset.model): 91 ↛ 92line 91 didn't jump to line 92 because the condition on line 91 was never true

92 return queryset 

93 

94 if queryset.ordered: 

95 return queryset 

96 

97 ordering = queryset.model._meta.ordering 

98 return queryset.order_by(*ordering)