diff --git a/CHANGELOG.md b/CHANGELOG.md index e3724ddf0f5..683090cebc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ v2.5.3 (FUTURE) ## Enhancements * [#1630](https://github.com/digitalocean/netbox/issues/1630) - Enable bulk editing of prefix/IP mask length +* [#1871](https://github.com/digitalocean/netbox/issues/1871) - Enable filtering sites by parent region * [#2693](https://github.com/digitalocean/netbox/issues/2693) - Additional cable colors * [#2726](https://github.com/digitalocean/netbox/issues/2726) - Include cables in global search diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 0d9deadc295..0f186f162d7 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -62,14 +62,14 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): choices=SITE_STATUS_CHOICES, null_value=None ) - region_id = django_filters.ModelMultipleChoiceFilter( - queryset=Region.objects.all(), + region_id = django_filters.NumberFilter( + method='filter_region', + field_name='pk', label='Region (ID)', ) - region = django_filters.ModelMultipleChoiceFilter( - field_name='region__slug', - queryset=Region.objects.all(), - to_field_name='slug', + region = django_filters.CharFilter( + method='filter_region', + field_name='slug', label='Region (slug)', ) tenant_id = django_filters.ModelMultipleChoiceFilter( @@ -108,6 +108,16 @@ def search(self, queryset, name, value): pass return queryset.filter(qs_filter) + def filter_region(self, queryset, name, value): + try: + region = Region.objects.get(**{name: value}) + except ObjectDoesNotExist: + return queryset.none() + return queryset.filter( + Q(region=region) | + Q(region__in=region.get_descendants()) + ) + class RackGroupFilter(django_filters.FilterSet): q = django_filters.CharFilter( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index d02235277d7..61feba08c3a 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -236,9 +236,10 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False ) region = FilterTreeNodeMultipleChoiceField( - queryset=Region.objects.annotate(filter_count=Count('sites')), + queryset=Region.objects.all(), to_field_name='slug', required=False, + count_attr='site_count' ) tenant = FilterChoiceField( queryset=Tenant.objects.annotate(filter_count=Count('sites')), diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 347f0c8b8b7..567f9046ef5 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -201,6 +201,13 @@ def to_csv(self): self.parent.name if self.parent else None, ) + @property + def site_count(self): + return Site.objects.filter( + Q(region=self) | + Q(region__in=self.get_descendants()) + ).count() + # # Sites diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 1d74c1c8598..93a5178f3ca 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -124,7 +124,7 @@ def post(self, request): # class RegionListView(ObjectListView): - queryset = Region.objects.annotate(site_count=Count('sites')) + queryset = Region.objects.all() filter = filters.RegionFilter filter_form = forms.RegionFilterForm table = tables.RegionTable diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 46dbcc78904..b531fa637ff 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -505,8 +505,9 @@ def __iter__(self): class FilterChoiceFieldMixin(object): iterator = FilterChoiceIterator - def __init__(self, null_label=None, *args, **kwargs): + def __init__(self, null_label=None, count_attr='filter_count', *args, **kwargs): self.null_label = null_label + self.count_attr = count_attr if 'required' not in kwargs: kwargs['required'] = False if 'widget' not in kwargs: @@ -515,8 +516,9 @@ def __init__(self, null_label=None, *args, **kwargs): def label_from_instance(self, obj): label = super().label_from_instance(obj) - if hasattr(obj, 'filter_count'): - return '{} ({})'.format(label, obj.filter_count) + obj_count = getattr(obj, self.count_attr, None) + if obj_count is not None: + return '{} ({})'.format(label, obj_count) return label