|
1 | 1 | from typing import Annotated, TYPE_CHECKING
|
2 | 2 |
|
3 |
| -from django.db.models import Q |
| 3 | +from django.db.models import Q, QuerySet |
4 | 4 | import strawberry
|
5 | 5 | import strawberry_django
|
6 | 6 | from strawberry.scalars import ID
|
7 |
| -from strawberry_django import FilterLookup |
| 7 | +from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, FilterLookup |
8 | 8 |
|
9 | 9 | from core.graphql.filter_mixins import ChangeLogFilterMixin
|
10 | 10 | from dcim import models
|
|
19 | 19 | WeightFilterMixin,
|
20 | 20 | )
|
21 | 21 | from tenancy.graphql.filter_mixins import TenancyFilterMixin, ContactFilterMixin
|
| 22 | +from utilities.query import count_related |
22 | 23 | from .filter_mixins import (
|
23 | 24 | CabledObjectModelFilterMixin,
|
24 | 25 | ComponentModelFilterMixin,
|
@@ -326,6 +327,9 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
|
326 | 327 | )
|
327 | 328 | default_platform_id: ID | None = strawberry_django.filter_field()
|
328 | 329 | part_number: FilterLookup[str] | None = strawberry_django.filter_field()
|
| 330 | + instances: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( |
| 331 | + strawberry_django.filter_field() |
| 332 | + ) |
329 | 333 | u_height: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
330 | 334 | strawberry_django.filter_field()
|
331 | 335 | )
|
@@ -384,6 +388,51 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
|
384 | 388 | module_bay_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
|
385 | 389 | inventory_item_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
|
386 | 390 |
|
| 391 | + @strawberry_django.filter_field |
| 392 | + def has_instances( |
| 393 | + self, |
| 394 | + queryset: QuerySet[models.DeviceType], |
| 395 | + value: BaseFilterLookup[bool], |
| 396 | + prefix: str, |
| 397 | + ) -> tuple[QuerySet[models.DeviceType], Q]: |
| 398 | + """ |
| 399 | + Filters a queryset of device types based on whether they have associated instances. |
| 400 | + """ |
| 401 | + # Annotate each DeviceType with the number of Device instances which use the DeviceType |
| 402 | + qs = queryset.annotate(instance_count=count_related(models.Device, "device_type")) |
| 403 | + |
| 404 | + # IMPORTANT: read the actual boolean from the lookup container |
| 405 | + exact = getattr(value, "exact", None) |
| 406 | + if exact is None: |
| 407 | + return qs, Q() |
| 408 | + |
| 409 | + cond = Q(**{f"{prefix}instance_count__gt": 0}) if exact else Q(**{f"{prefix}instance_count": 0}) |
| 410 | + return qs, cond |
| 411 | + |
| 412 | + @strawberry_django.filter_field |
| 413 | + def instance_count( |
| 414 | + self, |
| 415 | + info, |
| 416 | + queryset: QuerySet[models.DeviceType], |
| 417 | + value: ComparisonFilterLookup[int], |
| 418 | + prefix: str, |
| 419 | + ) -> tuple[QuerySet[models.DeviceType], Q]: |
| 420 | + """ |
| 421 | + Filter by the number of related Device instances. |
| 422 | +
|
| 423 | + Annotates each DeviceType with instance_count and applies comparison lookups |
| 424 | + (exact, gt, gte, lt, lte, range). |
| 425 | + """ |
| 426 | + # Annotate each DeviceType with the number of Device instances which use the DeviceType |
| 427 | + qs = queryset.annotate(instance_count=count_related(models.Device, "device_type")) |
| 428 | + # NOTE: include the trailing "__" so Strawberry-Django appends lookups correctly |
| 429 | + return strawberry_django.process_filters( |
| 430 | + filters=value, |
| 431 | + queryset=qs, |
| 432 | + info=info, |
| 433 | + prefix=f"{prefix}instance_count__", |
| 434 | + ) |
| 435 | + |
387 | 436 |
|
388 | 437 | @strawberry_django.filter_type(models.FrontPort, lookups=True)
|
389 | 438 | class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
@@ -665,6 +714,9 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
|
665 | 714 | profile_id: ID | None = strawberry_django.filter_field()
|
666 | 715 | model: FilterLookup[str] | None = strawberry_django.filter_field()
|
667 | 716 | part_number: FilterLookup[str] | None = strawberry_django.filter_field()
|
| 717 | + instances: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( |
| 718 | + strawberry_django.filter_field() |
| 719 | + ) |
668 | 720 | airflow: Annotated['ModuleAirflowEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
669 | 721 | strawberry_django.filter_field()
|
670 | 722 | )
|
@@ -699,6 +751,51 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
|
699 | 751 | Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
700 | 752 | ) = strawberry_django.filter_field()
|
701 | 753 |
|
| 754 | + @strawberry_django.filter_field |
| 755 | + def has_instances( |
| 756 | + self, |
| 757 | + queryset: QuerySet[models.ModuleType], |
| 758 | + value: BaseFilterLookup[bool], |
| 759 | + prefix: str, |
| 760 | + ) -> tuple[QuerySet[models.ModuleType], Q]: |
| 761 | + """ |
| 762 | + Filters a queryset of module types based on whether they have associated instances. |
| 763 | + """ |
| 764 | + # Annotate each ModuleType with the number of Module instances which use the ModuleType |
| 765 | + qs = queryset.annotate(instance_count=count_related(models.Module, "module_type")) |
| 766 | + |
| 767 | + # IMPORTANT: read the actual boolean from the lookup container |
| 768 | + exact = getattr(value, "exact", None) |
| 769 | + if exact is None: |
| 770 | + return qs, Q() |
| 771 | + |
| 772 | + cond = Q(**{f"{prefix}instance_count__gt": 0}) if exact else Q(**{f"{prefix}instance_count": 0}) |
| 773 | + return qs, cond |
| 774 | + |
| 775 | + @strawberry_django.filter_field |
| 776 | + def instance_count( |
| 777 | + self, |
| 778 | + info, |
| 779 | + queryset: QuerySet[models.ModuleType], |
| 780 | + value: ComparisonFilterLookup[int], |
| 781 | + prefix: str, |
| 782 | + ) -> tuple[QuerySet[models.ModuleType], Q]: |
| 783 | + """ |
| 784 | + Filter by the number of related Module instances. |
| 785 | +
|
| 786 | + Annotates each ModuleType with instance_count and applies comparison lookups |
| 787 | + (exact, gt, gte, lt, lte, range). |
| 788 | + """ |
| 789 | + # Annotate each ModuleType with the number of Module instances which use the ModuleType |
| 790 | + qs = queryset.annotate(instance_count=count_related(models.Module, "module_type")) |
| 791 | + # NOTE: include the trailing "__" so Strawberry-Django appends lookups correctly |
| 792 | + return strawberry_django.process_filters( |
| 793 | + filters=value, |
| 794 | + queryset=qs, |
| 795 | + info=info, |
| 796 | + prefix=f"{prefix}instance_count__", |
| 797 | + ) |
| 798 | + |
702 | 799 |
|
703 | 800 | @strawberry_django.filter_type(models.Platform, lookups=True)
|
704 | 801 | class PlatformFilter(OrganizationalModelFilterMixin):
|
|
0 commit comments