Skip to content

# `nautobot.apps.filters`

Filterset base classes and mixins for app implementation.

## `nautobot.apps.filters.BaseFilterSet`

Bases: `django_filters.FilterSet`

A base filterset which provides common functionality to all Nautobot filtersets.

Source code in `nautobot/core/filters.py`

    533
    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    555
    556
    557
    558
    559
    560
    561
    562
    563
    564
    565
    566
    567
    568
    569
    570
    571
    572
    573
    574
    575
    576
    577
    578
    579
    580
    581
    582
    583
    584
    585
    586
    587
    588
    589
    590
    591
    592
    593
    594
    595
    596
    597
    598
    599
    600
    601
    602
    603
    604
    605
    606
    607
    608
    609
    610
    611
    612
    613
    614
    615
    616
    617
    618
    619
    620
    621
    622
    623
    624
    625
    626
    627
    628
    629
    630
    631
    632
    633
    634
    635
    636
    637
    638
    639
    640
    641
    642
    643
    644
    645
    646
    647
    648
    649
    650
    651
    652
    653
    654
    655
    656
    657
    658
    659
    660
    661
    662
    663
    664
    665
    666
    667
    668
    669
    670
    671
    672
    673
    674
    675
    676
    677
    678
    679
    680
    681
    682
    683
    684
    685
    686
    687
    688
    689
    690
    691
    692
    693
    694
    695
    696
    697
    698
    699
    700
    701
    702
    703
    704
    705
    706
    707
    708
    709
    710
    711
    712
    713
    714
    715
    716
    717
    718
    719
    720
    721
    722
    723
    724
    725
    726
    727
    728
    729
    730
    731
    732
    733
    734
    735
    736
    737
    738
    739
    740
    741
    742
    743
    744
    745
    746
    747
    748
    749
    750
    751
    752
    753
    754
    755
    756
    757
    758
    759
    760
    761
    762
    763
    764
    765
    766
    767
    class BaseFilterSet(django_filters.FilterSet):
        """
        A base filterset which provides common functionality to all Nautobot filtersets.
        """

        FILTER_DEFAULTS = deepcopy(django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS)
        FILTER_DEFAULTS.update(
            {
                models.AutoField: {"filter_class": MultiValueNumberFilter},
                models.BigIntegerField: {"filter_class": MultiValueBigNumberFilter},
                models.CharField: {"filter_class": MultiValueCharFilter},
                models.DateField: {"filter_class": MultiValueDateFilter},
                models.DateTimeField: {"filter_class": MultiValueDateTimeFilter},
                models.DecimalField: {"filter_class": MultiValueDecimalFilter},
                models.EmailField: {"filter_class": MultiValueCharFilter},
                models.FloatField: {"filter_class": MultiValueFloatFilter},
                models.IntegerField: {"filter_class": MultiValueNumberFilter},
                # Ref: https://github.com/carltongibson/django-filter/issues/1107
                models.JSONField: {"filter_class": MultiValueCharFilter, "extra": lambda f: {"lookup_expr": "icontains"}},
                models.PositiveIntegerField: {"filter_class": MultiValueNumberFilter},
                models.PositiveSmallIntegerField: {"filter_class": MultiValueNumberFilter},
                models.SlugField: {"filter_class": MultiValueCharFilter},
                models.SmallIntegerField: {"filter_class": MultiValueNumberFilter},
                models.TextField: {"filter_class": MultiValueCharFilter},
                models.TimeField: {"filter_class": MultiValueTimeFilter},
                models.URLField: {"filter_class": MultiValueCharFilter},
                models.UUIDField: {"filter_class": MultiValueUUIDFilter},
                core_fields.MACAddressCharField: {"filter_class": MultiValueMACAddressFilter},
                core_fields.TagsField: {"filter_class": TagFilter},
            }
        )

        @staticmethod
        def _get_filter_lookup_dict(existing_filter):
            # Choose the lookup expression map based on the filter type
            if isinstance(
                existing_filter,
                (
                    MultiValueDateFilter,
                    MultiValueDateTimeFilter,
                    MultiValueDecimalFilter,
                    MultiValueFloatFilter,
                    MultiValueNumberFilter,
                    MultiValueTimeFilter,
                ),
            ):
                lookup_map = constants.FILTER_NUMERIC_BASED_LOOKUP_MAP

            # These filter types support only negation
            elif isinstance(
                existing_filter,
                (
                    django_filters.ModelChoiceFilter,
                    django_filters.ModelMultipleChoiceFilter,
                    TagFilter,
                    TreeNodeMultipleChoiceFilter,
                ),
            ):
                lookup_map = constants.FILTER_NEGATION_LOOKUP_MAP

            elif isinstance(
                existing_filter,
                (
                    django_filters.filters.CharFilter,
                    django_filters.MultipleChoiceFilter,
                    MultiValueCharFilter,
                    MultiValueMACAddressFilter,
                ),
            ):
                lookup_map = constants.FILTER_CHAR_BASED_LOOKUP_MAP

            else:
                lookup_map = None

            return lookup_map

        @classmethod
        def _generate_lookup_expression_filters(cls, filter_name, filter_field):
            """
            For specific filter types, new filters are created based on defined lookup expressions in
            the form `<field_name>__<lookup_expr>`
            """
            magic_filters = {}
            if filter_field.method is not None or filter_field.lookup_expr not in ["exact", "in"]:
                return magic_filters

            # Choose the lookup expression map based on the filter type
            lookup_map = cls._get_filter_lookup_dict(filter_field)
            if lookup_map is None:
                # Do not augment this filter type with more lookup expressions
                return magic_filters

            # Get properties of the existing filter for later use
            field_name = filter_field.field_name
            field = get_model_field(cls._meta.model, field_name)

            # If there isn't a model field, return.
            if field is None:
                return magic_filters

            # If the field allows null values, add an `isnull`` check
            if getattr(field, "null", None):
                # Use this method vs extend as the `lookup_map` variable is generally one of
                # the constants which we do not want to update
                lookup_map = dict(lookup_map, isnull="isnull")

            # Create new filters for each lookup expression in the map
            for lookup_name, lookup_expr in lookup_map.items():
                new_filter_name = f"{filter_name}__{lookup_name}"

                try:
                    if filter_name in cls.declared_filters and lookup_expr not in {"isnull"}:
                        # The filter field has been explicitly defined on the filterset class so we must manually
                        # create the new filter with the same type because there is no guarantee the defined type
                        # is the same as the default type for the field. This does not apply if the filter
                        # should retain the original lookup_expr type, such as `isnull` using a boolean field on a
                        # char or date object.
                        resolve_field(field, lookup_expr)  # Will raise FieldLookupError if the lookup is invalid
                        new_filter = type(filter_field)(
                            field_name=field_name,
                            lookup_expr=lookup_expr,
                            label=filter_field.label,
                            exclude=filter_field.exclude,
                            distinct=filter_field.distinct,
                            **filter_field.extra,
                        )
                    else:
                        # The filter field is listed in Meta.fields so we can safely rely on default behavior
                        # Will raise FieldLookupError if the lookup is invalid
                        new_filter = cls.filter_for_field(field, field_name, lookup_expr)
                except django_filters.exceptions.FieldLookupError:
                    # The filter could not be created because the lookup expression is not supported on the field
                    continue

                if lookup_name.startswith("n"):
                    # This is a negation filter which requires a queryset.exclude() clause
                    # Of course setting the negation of the existing filter's exclude attribute handles both cases
                    new_filter.exclude = not filter_field.exclude

                magic_filters[new_filter_name] = new_filter

            return magic_filters

        @classmethod
        def add_filter(cls, new_filter_name, new_filter_field):
            """
            Allow filters to be added post-generation on import.

            Will provide `<field_name>__<lookup_expr>` generation automagically.
            """
            if not isinstance(new_filter_field, django_filters.Filter):
                raise TypeError(f"Tried to add filter ({new_filter_name}) which is not an instance of Django Filter")

            if new_filter_name in cls.base_filters:
                raise AttributeError(
                    f"There was a conflict with filter `{new_filter_name}`, the custom filter was ignored."
                )

            cls.base_filters[new_filter_name] = new_filter_field
            # django-filters has no concept of "abstract" filtersets, so we have to fake it
            if cls._meta.model is not None:
                cls.base_filters.update(
                    cls._generate_lookup_expression_filters(filter_name=new_filter_name, filter_field=new_filter_field)
                )

        @classmethod
        def get_fields(cls):
            fields = super().get_fields()
            if "id" not in fields and (cls._meta.exclude is None or "id" not in cls._meta.exclude):
                # Add "id" as the first key in the `fields` OrderedDict
                fields = OrderedDict(id=[django_filters.conf.settings.DEFAULT_LOOKUP_EXPR], **fields)
            return fields

        @classmethod
        def get_filters(cls):
            """
            Override filter generation to support dynamic lookup expressions for certain filter types.
            """
            filters = super().get_filters()

            # django-filters has no concept of "abstract" filtersets, so we have to fake it
            if cls._meta.model is not None:
                new_filters = {}
                for existing_filter_name, existing_filter in filters.items():
                    new_filters.update(
                        cls._generate_lookup_expression_filters(
                            filter_name=existing_filter_name,
                            filter_field=existing_filter,
                        )
                    )

                filters.update(new_filters)

            return filters

        @classmethod
        def filter_for_lookup(cls, field, lookup_type):
            """Override filter_for_lookup method to set ChoiceField Filter to MultipleChoiceFilter.

            Note: Any CharField or IntegerField with choices set is a ChoiceField.
            """
            if lookup_type == "exact" and getattr(field, "choices", None):
                return django_filters.MultipleChoiceFilter, {"choices": field.choices}
            return super().filter_for_lookup(field, lookup_type)

        def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
            super().__init__(data, queryset, request=request, prefix=prefix)
            self._is_valid = None
            self._errors = None

        def is_valid(self):
            """Extend FilterSet.is_valid() to potentially enforce settings.STRICT_FILTERING."""
            if self._is_valid is None:
                self._is_valid = super().is_valid()
                if settings.STRICT_FILTERING:
                    self._is_valid = self._is_valid and set(self.form.data.keys()).issubset(self.form.cleaned_data.keys())
                else:
                    # Trigger warning logs associated with generating self.errors
                    self.errors
            return self._is_valid

        @property
        def errors(self):
            """Extend FilterSet.errors to potentially include additional errors from settings.STRICT_FILTERING."""
            if self._errors is None:
                self._errors = ErrorDict(self.form.errors)
                for extra_key in set(self.form.data.keys()).difference(self.form.cleaned_data.keys()):
                    # If a given field was invalid, it will be omitted from cleaned_data; don't report extra errors
                    if extra_key not in self._errors:
                        if settings.STRICT_FILTERING:
                            self._errors.setdefault(extra_key, ErrorList()).append("Unknown filter field")
                        else:
                            logger.warning('%s: Unknown filter field "%s"', self.__class__.__name__, extra_key)

            return self._errors

### `errors` `property`

Extend FilterSet.errors to potentially include additional errors from settings.STRICT_FILTERING.

### `add_filter(new_filter_name, new_filter_field)` `classmethod`

Allow filters to be added post-generation on import.

Will provide `<field_name>__<lookup_expr>` generation automagically.

Source code in `nautobot/core/filters.py`

    676
    677
    678
    679
    680
    681
    682
    683
    684
    685
    686
    687
    688
    689
    690
    691
    692
    693
    694
    695
    696
    @classmethod
    def add_filter(cls, new_filter_name, new_filter_field):
        """
        Allow filters to be added post-generation on import.

        Will provide `<field_name>__<lookup_expr>` generation automagically.
        """
        if not isinstance(new_filter_field, django_filters.Filter):
            raise TypeError(f"Tried to add filter ({new_filter_name}) which is not an instance of Django Filter")

        if new_filter_name in cls.base_filters:
            raise AttributeError(
                f"There was a conflict with filter `{new_filter_name}`, the custom filter was ignored."
            )

        cls.base_filters[new_filter_name] = new_filter_field
        # django-filters has no concept of "abstract" filtersets, so we have to fake it
        if cls._meta.model is not None:
            cls.base_filters.update(
                cls._generate_lookup_expression_filters(filter_name=new_filter_name, filter_field=new_filter_field)
            )

### `filter_for_lookup(field, lookup_type)` `classmethod`

Override filter_for_lookup method to set ChoiceField Filter to MultipleChoiceFilter.

Note: Any CharField or IntegerField with choices set is a ChoiceField.

Source code in `nautobot/core/filters.py`

    728
    729
    730
    731
    732
    733
    734
    735
    736
    @classmethod
    def filter_for_lookup(cls, field, lookup_type):
        """Override filter_for_lookup method to set ChoiceField Filter to MultipleChoiceFilter.

        Note: Any CharField or IntegerField with choices set is a ChoiceField.
        """
        if lookup_type == "exact" and getattr(field, "choices", None):
            return django_filters.MultipleChoiceFilter, {"choices": field.choices}
        return super().filter_for_lookup(field, lookup_type)

### `get_filters()` `classmethod`

Override filter generation to support dynamic lookup expressions for certain filter types.

Source code in `nautobot/core/filters.py`

    706
    707
    708
    709
    710
    711
    712
    713
    714
    715
    716
    717
    718
    719
    720
    721
    722
    723
    724
    725
    726
    @classmethod
    def get_filters(cls):
        """
        Override filter generation to support dynamic lookup expressions for certain filter types.
        """
        filters = super().get_filters()

        # django-filters has no concept of "abstract" filtersets, so we have to fake it
        if cls._meta.model is not None:
            new_filters = {}
            for existing_filter_name, existing_filter in filters.items():
                new_filters.update(
                    cls._generate_lookup_expression_filters(
                        filter_name=existing_filter_name,
                        filter_field=existing_filter,
                    )
                )

            filters.update(new_filters)

        return filters

### `is_valid()`

Extend FilterSet.is_valid() to potentially enforce settings.STRICT_FILTERING.

Source code in `nautobot/core/filters.py`

    743
    744
    745
    746
    747
    748
    749
    750
    751
    752
    def is_valid(self):
        """Extend FilterSet.is_valid() to potentially enforce settings.STRICT_FILTERING."""
        if self._is_valid is None:
            self._is_valid = super().is_valid()
            if settings.STRICT_FILTERING:
                self._is_valid = self._is_valid and set(self.form.data.keys()).issubset(self.form.cleaned_data.keys())
            else:
                # Trigger warning logs associated with generating self.errors
                self.errors
        return self._is_valid

## `nautobot.apps.filters.ConfigContextRoleFilter`

Bases: `NaturalKeyOrPKMultipleChoiceFilter`

Limit role choices to the available role choices for Device and VM

Source code in `nautobot/extras/filters/mixins.py`

    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    class ConfigContextRoleFilter(NaturalKeyOrPKMultipleChoiceFilter):
        """Limit role choices to the available role choices for Device and VM"""

        def __init__(self, *args, **kwargs):
            kwargs.setdefault("field_name", "roles")
            kwargs.setdefault("queryset", Role.objects.get_for_models([Device, VirtualMachine]))
            kwargs.setdefault("label", "Role (name or ID)")
            kwargs.setdefault("to_field_name", "name")

            super().__init__(*args, **kwargs)

## `nautobot.apps.filters.ContentTypeChoiceFilter`

Bases: `ContentTypeFilterMixin`, `django_filters.ChoiceFilter`

Allows character-based ContentType filtering by . (e.g. "dcim.location") but an explicit set of choices must be provided.

Example use on a `FilterSet`:

    content_type = ContentTypeChoiceFilter(
        choices=FeatureQuery("dynamic_groups").get_choices,
    )

Source code in `nautobot/core/filters.py`

    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    class ContentTypeChoiceFilter(ContentTypeFilterMixin, django_filters.ChoiceFilter):
        """
        Allows character-based ContentType filtering by <app_label>.<model> (e.g.
        "dcim.location") but an explicit set of choices must be provided.

        Example use on a `FilterSet`:

            content_type = ContentTypeChoiceFilter(
                choices=FeatureQuery("dynamic_groups").get_choices,
            )
        """

## `nautobot.apps.filters.ContentTypeFilter`

Bases: `ContentTypeFilterMixin`, `django_filters.CharFilter`

Allows character-based ContentType filtering by . (e.g. "dcim.location").

Does not support limiting of choices. Can be used without arguments on a `FilterSet`:

    content_type = ContentTypeFilter()

Source code in `nautobot/core/filters.py`

    192
    193
    194
    195
    196
    197
    198
    199
    class ContentTypeFilter(ContentTypeFilterMixin, django_filters.CharFilter):
        """
        Allows character-based ContentType filtering by <app_label>.<model> (e.g. "dcim.location").

        Does not support limiting of choices. Can be used without arguments on a `FilterSet`:

            content_type = ContentTypeFilter()
        """

## `nautobot.apps.filters.ContentTypeFilterMixin`

Mixin to allow specifying a ContentType by . (e.g. "dcim.location").

Source code in `nautobot/core/filters.py`

    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    class ContentTypeFilterMixin:
        """
        Mixin to allow specifying a ContentType by <app_label>.<model> (e.g. "dcim.location").
        """

        def filter(self, qs, value):
            if value in EMPTY_VALUES:
                return qs

            try:
                app_label, model = value.lower().split(".")
            except ValueError:
                return qs.none()

            return qs.filter(
                **{
                    f"{self.field_name}__app_label": app_label,
                    f"{self.field_name}__model": model,
                }
            )

## `nautobot.apps.filters.ContentTypeMultipleChoiceFilter`

Bases: `django_filters.MultipleChoiceFilter`

Allows multiple-choice ContentType filtering by . (e.g. "dcim.location").

Defaults to joining multiple options with "AND". Pass `conjoined=False` to override this behavior to join with "OR" instead.

Example use on a `FilterSet`:

    content_types = ContentTypeMultipleChoiceFilter(
        choices=FeatureQuery("statuses").get_choices,
    )

Source code in `nautobot/core/filters.py`

    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    class ContentTypeMultipleChoiceFilter(django_filters.MultipleChoiceFilter):
        """
        Allows multiple-choice ContentType filtering by <app_label>.<model> (e.g. "dcim.location").

        Defaults to joining multiple options with "AND". Pass `conjoined=False` to
        override this behavior to join with "OR" instead.

        Example use on a `FilterSet`:

            content_types = ContentTypeMultipleChoiceFilter(
                choices=FeatureQuery("statuses").get_choices,
            )
        """

        def __init__(self, *args, **kwargs):
            kwargs.setdefault("conjoined", True)
            super().__init__(*args, **kwargs)

        def filter(self, qs, value):
            """Filter on value, which should be list of content-type names.

            e.g. `['dcim.device', 'dcim.rack']`
            """
            if not self.conjoined:
                q = models.Q()

            for v in value:
                if self.conjoined:
                    qs = ContentTypeFilter.filter(self, qs, v)
                else:
                    # Similar to the ContentTypeFilter.filter() call above, but instead of narrowing the query each time
                    # (a AND b AND c ...) we broaden the query each time (a OR b OR c ...).
                    # Specifically, we're mapping a value like ['dcim.device', 'ipam.vlan'] to a query like
                    # Q((field__app_label="dcim" AND field__model="device") OR (field__app_label="ipam" AND field__model="VLAN"))
                    try:
                        app_label, model = v.lower().split(".")
                    except ValueError:
                        continue
                    q |= models.Q(
                        **{
                            f"{self.field_name}__app_label": app_label,
                            f"{self.field_name}__model": model,
                        }
                    )

            if not self.conjoined:
                qs = qs.filter(q)

            return qs

### `filter(qs, value)`

Filter on value, which should be list of content-type names.

e.g. `['dcim.device', 'dcim.rack']`

Source code in `nautobot/core/filters.py`

    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    def filter(self, qs, value):
        """Filter on value, which should be list of content-type names.

        e.g. `['dcim.device', 'dcim.rack']`
        """
        if not self.conjoined:
            q = models.Q()

        for v in value:
            if self.conjoined:
                qs = ContentTypeFilter.filter(self, qs, v)
            else:
                # Similar to the ContentTypeFilter.filter() call above, but instead of narrowing the query each time
                # (a AND b AND c ...) we broaden the query each time (a OR b OR c ...).
                # Specifically, we're mapping a value like ['dcim.device', 'ipam.vlan'] to a query like
                # Q((field__app_label="dcim" AND field__model="device") OR (field__app_label="ipam" AND field__model="VLAN"))
                try:
                    app_label, model = v.lower().split(".")
                except ValueError:
                    continue
                q |= models.Q(
                    **{
                        f"{self.field_name}__app_label": app_label,
                        f"{self.field_name}__model": model,
                    }
                )

        if not self.conjoined:
            qs = qs.filter(q)

        return qs

## `nautobot.apps.filters.CustomFieldModelFilterSetMixin`

Bases: `django_filters.FilterSet`

Dynamically add a Filter for each CustomField applicable to the parent model. Add filters for extra lookup expressions on supported CustomField types.

Source code in `nautobot/extras/filters/mixins.py`

     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    class CustomFieldModelFilterSetMixin(django_filters.FilterSet):
        """
        Dynamically add a Filter for each CustomField applicable to the parent model. Add filters for
        extra lookup expressions on supported CustomField types.
        """

        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)

            custom_field_filter_classes = {
                CustomFieldTypeChoices.TYPE_DATE: CustomFieldDateFilter,
                CustomFieldTypeChoices.TYPE_BOOLEAN: CustomFieldBooleanFilter,
                CustomFieldTypeChoices.TYPE_INTEGER: CustomFieldNumberFilter,
                CustomFieldTypeChoices.TYPE_JSON: CustomFieldJSONFilter,
                CustomFieldTypeChoices.TYPE_MULTISELECT: CustomFieldMultiSelectFilter,
                CustomFieldTypeChoices.TYPE_SELECT: CustomFieldMultiSelectFilter,
            }

            custom_fields = CustomField.objects.get_for_model(self._meta.model, exclude_filter_disabled=True)
            for cf in custom_fields:
                # Determine filter class for this CustomField type, default to CustomFieldCharFilter
                new_filter_name = cf.add_prefix_to_cf_key()
                filter_class = custom_field_filter_classes.get(cf.type, CustomFieldCharFilter)
                new_filter = filter_class(field_name=cf.key, custom_field=cf)
                new_filter.label = f"{cf.label}"
                # Create base filter (cf_customfieldname)
                self.filters[new_filter_name] = new_filter

                # Create extra lookup expression filters (cf_customfieldname__lookup_expr)
                self.filters.update(
                    self._generate_custom_field_lookup_expression_filters(filter_name=new_filter_name, custom_field=cf)
                )

        @staticmethod
        def _get_custom_field_filter_lookup_dict(filter_type):
            # Choose the lookup expression map based on the filter type
            if issubclass(filter_type, (CustomFieldMultiValueNumberFilter, CustomFieldMultiValueDateFilter)):
                return FILTER_NUMERIC_BASED_LOOKUP_MAP
            elif issubclass(filter_type, CustomFieldMultiSelectFilter):
                return FILTER_NEGATION_LOOKUP_MAP
            else:
                return FILTER_CHAR_BASED_LOOKUP_MAP

        # TODO 2.0: Transition CustomField filters to nautobot.core.filters.MultiValue* filters and
        # leverage BaseFilterSet to add dynamic lookup expression filters. Remove CustomField.filter_logic field
        @classmethod
        def _generate_custom_field_lookup_expression_filters(cls, filter_name, custom_field):
            """
            For specific filter types, new filters are created based on defined lookup expressions in
            the form `<field_name>__<lookup_expr>`. Copied from nautobot.core.filters.BaseFilterSet
            and updated to work with custom fields.
            """
            magic_filters = {}
            custom_field_type_to_filter_map = {
                CustomFieldTypeChoices.TYPE_DATE: CustomFieldMultiValueDateFilter,
                CustomFieldTypeChoices.TYPE_INTEGER: CustomFieldMultiValueNumberFilter,
                CustomFieldTypeChoices.TYPE_SELECT: CustomFieldMultiValueCharFilter,
                CustomFieldTypeChoices.TYPE_MULTISELECT: CustomFieldMultiSelectFilter,
                CustomFieldTypeChoices.TYPE_TEXT: CustomFieldMultiValueCharFilter,
                CustomFieldTypeChoices.TYPE_URL: CustomFieldMultiValueCharFilter,
            }

            if custom_field.type in custom_field_type_to_filter_map:
                filter_type = custom_field_type_to_filter_map[custom_field.type]
            else:
                return magic_filters

            # Choose the lookup expression map based on the filter type
            lookup_map = cls._get_custom_field_filter_lookup_dict(filter_type)

            # Create new filters for each lookup expression in the map
            for lookup_name, lookup_expr in lookup_map.items():
                new_filter_name = f"{filter_name}__{lookup_name}"
                new_filter = filter_type(
                    field_name=custom_field.key,
                    lookup_expr=lookup_expr,
                    custom_field=custom_field,
                    label=f"{custom_field.label} ({verbose_lookup_expr(lookup_expr)})",
                    exclude=lookup_name.startswith("n"),
                )

                magic_filters[new_filter_name] = new_filter

            return magic_filters

## `nautobot.apps.filters.FilterExtension`

Class that may be returned by a registered Filter Extension function.

Source code in `nautobot/extras/plugins/__init__.py`

    453
    454
    455
    456
    457
    458
    459
    460
    class FilterExtension:
        """Class that may be returned by a registered Filter Extension function."""

        model = None

        filterset_fields = {}

        filterform_fields = {}

## `nautobot.apps.filters.MappedPredicatesFilterMixin`

A filter mixin to provide the ability to specify fields and lookup expressions to use for filtering.

A mapping of filter predicates (field_name: lookup_expr) must be provided to the filter when declared on a filterset. This mapping is used to construct a `Q` query to filter based on the provided predicates.

By default a predicate for `{"id": "iexact"}` (`id__exact`) will always be included.

Example

q = SearchFilter( filter_predicates={ "comments": "icontains", "name": "icontains", }, )

Optionally you may also provide a callable to use as a preprocessor for the filter predicate by providing the value as a nested dict with "lookup_expr" and "preprocessor" keys. For example:

    q = SearchFilter(
        filter_predicates={
            "asn": {
                "lookup_expr": "exact",
                "preprocessor": int,
            },
        },
    )

This tells the filter to try to cast `asn` to an `int`. If it fails, this predicate will be skipped.

Source code in `nautobot/core/filters.py`

    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    class MappedPredicatesFilterMixin:
        """
        A filter mixin to provide the ability to specify fields and lookup expressions to use for
        filtering.

        A mapping of filter predicates (field_name: lookup_expr) must be provided to the filter when
        declared on a filterset. This mapping is used to construct a `Q` query to filter based on the
        provided predicates.

        By default a predicate for `{"id": "iexact"}` (`id__exact`) will always be included.

        Example:

            q = SearchFilter(
                filter_predicates={
                    "comments": "icontains",
                    "name": "icontains",
                },
            )

        Optionally you may also provide a callable to use as a preprocessor for the filter predicate by
        providing the value as a nested dict with "lookup_expr" and "preprocessor" keys. For example:

            q = SearchFilter(
                filter_predicates={
                    "asn": {
                        "lookup_expr": "exact",
                        "preprocessor": int,
                    },
                },
            )

        This tells the filter to try to cast `asn` to an `int`. If it fails, this predicate will be
        skipped.
        """

        # Optional label for the form element generated for this filter
        label = None

        # Filter predicates that will always be included if not otherwise specified.
        default_filter_predicates = {"id": "iexact"}

        # Lookup expressions for which whitespace should be preserved.
        preserve_whitespace = ["icontains"]

        def __init__(self, filter_predicates=None, strip=False, *args, **kwargs):
            if not isinstance(filter_predicates, dict):
                raise TypeError("filter_predicates must be a dict")

            # Layer incoming filter_predicates on top of the defaults so that any overrides take
            # precedence.
            defaults = deepcopy(self.default_filter_predicates)
            defaults.update(filter_predicates)

            # Format: {field_name: lookup_expr, ...}
            self.filter_predicates = defaults

            # Try to use the label from the class if it is defined.
            kwargs.setdefault("label", self.label)

            # Whether to strip whtespace in the inner CharField form (default: False)
            kwargs.setdefault("strip", strip)

            super().__init__(*args, **kwargs)

            # Generate the query with a sentinel value to validate it and surface parse errors.
            self.generate_query(value="")

        def generate_query(self, value, **kwargs):
            """
            Given a `value`, return a `Q` object for 2-tuple of `predicate=value`. Filter predicates are
            read from the instance filter. Any `kwargs` are ignored.
            """

            def noop(v):
                """Pass through the value."""
                return v

            query = models.Q()
            for field_name, lookup_info in self.filter_predicates.items():
                # Unless otherwise specified, set the default prepreprocssor
                if isinstance(lookup_info, str):
                    lookup_expr = lookup_info
                    if lookup_expr in self.preserve_whitespace:
                        preprocessor = noop
                    else:
                        preprocessor = str.strip

                # Or set it to what was defined by caller
                elif isinstance(lookup_info, dict):
                    lookup_expr = lookup_info.get("lookup_expr")
                    preprocessor = lookup_info.get("preprocessor")
                    if not callable(preprocessor):
                        raise TypeError("Preprocessor {preprocessor} must be callable!")
                else:
                    raise TypeError(f"Predicate value must be a str or a dict! Got: {type(lookup_info)}")

                # Try to preprocess the value or skip creating a predicate for it. In the event we try
                # to cast a value to an invalid type (e.g. `int("foo")` or `dict(42)`), ensure this
                # predicate is not included in the query.
                try:
                    new_value = preprocessor(value)
                except (TypeError, ValueError):
                    continue

                predicate = {f"{field_name}__{lookup_expr}": new_value}
                query |= models.Q(**predicate)

            # Return this for later use (such as introspection or debugging)
            return query

        def filter(self, qs, value):
            if value in EMPTY_VALUES:
                return qs

            # Evaluate the query and stash it for later use (such as introspection or debugging)
            query = self.generate_query(value=value)
            qs = self.get_method(qs)(query)
            self._most_recent_query = query
            return qs.distinct()

### `generate_query(value, **kwargs)`

Given a `value`, return a `Q` object for 2-tuple of `predicate=value`. Filter predicates are read from the instance filter. Any `kwargs` are ignored.

Source code in `nautobot/core/filters.py`

    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    def generate_query(self, value, **kwargs):
        """
        Given a `value`, return a `Q` object for 2-tuple of `predicate=value`. Filter predicates are
        read from the instance filter. Any `kwargs` are ignored.
        """

        def noop(v):
            """Pass through the value."""
            return v

        query = models.Q()
        for field_name, lookup_info in self.filter_predicates.items():
            # Unless otherwise specified, set the default prepreprocssor
            if isinstance(lookup_info, str):
                lookup_expr = lookup_info
                if lookup_expr in self.preserve_whitespace:
                    preprocessor = noop
                else:
                    preprocessor = str.strip

            # Or set it to what was defined by caller
            elif isinstance(lookup_info, dict):
                lookup_expr = lookup_info.get("lookup_expr")
                preprocessor = lookup_info.get("preprocessor")
                if not callable(preprocessor):
                    raise TypeError("Preprocessor {preprocessor} must be callable!")
            else:
                raise TypeError(f"Predicate value must be a str or a dict! Got: {type(lookup_info)}")

            # Try to preprocess the value or skip creating a predicate for it. In the event we try
            # to cast a value to an invalid type (e.g. `int("foo")` or `dict(42)`), ensure this
            # predicate is not included in the query.
            try:
                new_value = preprocessor(value)
            except (TypeError, ValueError):
                continue

            predicate = {f"{field_name}__{lookup_expr}": new_value}
            query |= models.Q(**predicate)

        # Return this for later use (such as introspection or debugging)
        return query

## `nautobot.apps.filters.MultiValueBigNumberFilter`

Bases: `MultiValueNumberFilter`

Subclass of MultiValueNumberFilter used for BigInteger model fields.

Source code in `nautobot/core/filters.py`

    93
    94
    class MultiValueBigNumberFilter(MultiValueNumberFilter):
        """Subclass of MultiValueNumberFilter used for BigInteger model fields."""

## `nautobot.apps.filters.NameSearchFilterSet`

Bases: `django_filters.FilterSet`

A base class for adding the search method to models which only expose the `name` field in searches.

Source code in `nautobot/core/filters.py`

    770
    771
    772
    773
    774
    775
    class NameSearchFilterSet(django_filters.FilterSet):
        """
        A base class for adding the search method to models which only expose the `name` field in searches.
        """

        q = SearchFilter(filter_predicates={"name": "icontains"})

## `nautobot.apps.filters.NaturalKeyOrPKMultipleChoiceFilter`

Bases: `django_filters.ModelMultipleChoiceFilter`

Filter that supports filtering on values matching the `pk` field and another field of a foreign-key related object. The desired field is set using the `to_field_name` keyword argument on filter initialization (defaults to `name`).

Source code in `nautobot/core/filters.py`

    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    @extend_schema_field(OpenApiTypes.STR)
    class NaturalKeyOrPKMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
        """
        Filter that supports filtering on values matching the `pk` field and another
        field of a foreign-key related object. The desired field is set using the `to_field_name`
        keyword argument on filter initialization (defaults to `name`).
        """

        field_class = forms.MultiMatchModelMultipleChoiceField

        def __init__(self, *args, **kwargs):
            self.natural_key = kwargs.setdefault("to_field_name", "name")
            super().__init__(*args, **kwargs)

        def get_filter_predicate(self, v):
            """
            Override base filter behavior to force the filter to use the `pk` field instead of
            the natural key in the generated filter.
            """

            # Null value filtering
            if v is None:
                return {f"{self.field_name}__isnull": True}

            # If value is a model instance, stringify it to a pk.
            if isinstance(v, models.Model):
                logger.debug("Model instance detected. Casting to a PK.")
                v = str(v.pk)

            # Try to cast the value to a UUID and set `is_pk` boolean.
            try:
                uuid.UUID(str(v))
            except (AttributeError, TypeError, ValueError):
                logger.debug("Non-UUID value detected: Filtering using natural key")
                is_pk = False
            else:
                v = str(v)  # Cast possible UUID instance to a string
                is_pk = True

            # If it's not a pk, then it's a name and the filter predicate needs to be nested (e.g.
            # `{"location__name": "ams01"}`) so that it can be usable in `Q` objects.
            if not is_pk:
                name = f"{self.field_name}__{self.field.to_field_name}"
            else:
                logger.debug("UUID detected: Filtering using field name")
                name = self.field_name

            if name and self.lookup_expr != django_filters.conf.settings.DEFAULT_LOOKUP_EXPR:
                name = "__".join([name, self.lookup_expr])

            return {name: v}

### `get_filter_predicate(v)`

Override base filter behavior to force the filter to use the `pk` field instead of the natural key in the generated filter.

Source code in `nautobot/core/filters.py`

    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    def get_filter_predicate(self, v):
        """
        Override base filter behavior to force the filter to use the `pk` field instead of
        the natural key in the generated filter.
        """

        # Null value filtering
        if v is None:
            return {f"{self.field_name}__isnull": True}

        # If value is a model instance, stringify it to a pk.
        if isinstance(v, models.Model):
            logger.debug("Model instance detected. Casting to a PK.")
            v = str(v.pk)

        # Try to cast the value to a UUID and set `is_pk` boolean.
        try:
            uuid.UUID(str(v))
        except (AttributeError, TypeError, ValueError):
            logger.debug("Non-UUID value detected: Filtering using natural key")
            is_pk = False
        else:
            v = str(v)  # Cast possible UUID instance to a string
            is_pk = True

        # If it's not a pk, then it's a name and the filter predicate needs to be nested (e.g.
        # `{"location__name": "ams01"}`) so that it can be usable in `Q` objects.
        if not is_pk:
            name = f"{self.field_name}__{self.field.to_field_name}"
        else:
            logger.debug("UUID detected: Filtering using field name")
            name = self.field_name

        if name and self.lookup_expr != django_filters.conf.settings.DEFAULT_LOOKUP_EXPR:
            name = "__".join([name, self.lookup_expr])

        return {name: v}

## `nautobot.apps.filters.NautobotFilterSet`

Bases: `BaseFilterSet`, `CreatedUpdatedModelFilterSetMixin`, `RelationshipModelFilterSetMixin`, `CustomFieldModelFilterSetMixin`

This class exists to combine common functionality and is used as a base class throughout the codebase where all of BaseFilterSet, CreatedUpdatedModelFilterSetMixin, RelationshipModelFilterSetMixin and CustomFieldModelFilterSetMixin are needed.

Source code in `nautobot/extras/filters/__init__.py`

    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    class NautobotFilterSet(
        BaseFilterSet,
        CreatedUpdatedModelFilterSetMixin,
        RelationshipModelFilterSetMixin,
        CustomFieldModelFilterSetMixin,
    ):
        """
        This class exists to combine common functionality and is used as a base class throughout the codebase where all of
        BaseFilterSet, CreatedUpdatedModelFilterSetMixin, RelationshipModelFilterSetMixin and CustomFieldModelFilterSetMixin
        are needed.
        """

## `nautobot.apps.filters.NumericArrayFilter`

Bases: `django_filters.NumberFilter`

Filter based on the presence of an integer within an ArrayField.

Source code in `nautobot/core/filters.py`

    159
    160
    161
    162
    163
    164
    165
    166
    167
    class NumericArrayFilter(django_filters.NumberFilter):
        """
        Filter based on the presence of an integer within an ArrayField.
        """

        def filter(self, qs, value):
            if value:
                value = [value]
            return super().filter(qs, value)

## `nautobot.apps.filters.RelatedMembershipBooleanFilter`

Bases: `django_filters.BooleanFilter`

BooleanFilter for related objects that will explicitly perform `exclude=True` and `isnull` lookups. The `field_name` argument is required and must be set to the related field on the model.

This should be used instead of a default `BooleanFilter` paired `method=` argument to test for the existence of related objects.

Example

has_interfaces = RelatedMembershipBooleanFilter( field_name="interfaces", label="Has interfaces", )

Source code in `nautobot/core/filters.py`

    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    class RelatedMembershipBooleanFilter(django_filters.BooleanFilter):
        """
        BooleanFilter for related objects that will explicitly perform `exclude=True` and `isnull`
        lookups. The `field_name` argument is required and must be set to the related field on the
        model.

        This should be used instead of a default `BooleanFilter` paired `method=`
        argument to test for the existence of related objects.

        Example:

            has_interfaces = RelatedMembershipBooleanFilter(
                field_name="interfaces",
                label="Has interfaces",
            )
        """

        def __init__(
            self, field_name=None, lookup_expr="isnull", *, label=None, method=None, distinct=False, exclude=True, **kwargs
        ):
            if field_name is None:
                raise ValueError(f"Field name is required for {self.__class__.__name__}")

            super().__init__(
                field_name=field_name,
                lookup_expr=lookup_expr,
                label=label,
                method=method,
                distinct=distinct,
                exclude=exclude,
                widget=forms.StaticSelect2(choices=forms.BOOLEAN_CHOICES),
                **kwargs,
            )

## `nautobot.apps.filters.RelationshipFilter`

Bases: `django_filters.ModelMultipleChoiceFilter`

Filter objects by the presence of associations on a given Relationship.

Source code in `nautobot/extras/filters/mixins.py`

    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    class RelationshipFilter(django_filters.ModelMultipleChoiceFilter):
        """
        Filter objects by the presence of associations on a given Relationship.
        """

        def __init__(self, side, relationship=None, queryset=None, qs=None, *args, **kwargs):
            self.relationship = relationship
            self.qs = qs
            self.side = side
            super().__init__(queryset=queryset, *args, **kwargs)

        def filter(self, qs, value):
            value = [entry.id for entry in value]
            # Check if value is empty or a DynamicChoiceField that is empty.
            if not value or "" in value:
                # if value is empty we return the entire unmodified queryset
                return qs
            else:
                if self.side == "source":
                    values = RelationshipAssociation.objects.filter(
                        destination_id__in=value,
                        source_type=self.relationship.source_type,
                        relationship=self.relationship,
                    ).values_list("source_id", flat=True)
                elif self.side == "destination":
                    values = RelationshipAssociation.objects.filter(
                        source_id__in=value,
                        destination_type=self.relationship.destination_type,
                        relationship=self.relationship,
                    ).values_list("destination_id", flat=True)
                else:
                    destinations = RelationshipAssociation.objects.filter(
                        source_id__in=value,
                        destination_type=self.relationship.destination_type,
                        relationship=self.relationship,
                    ).values_list("destination_id", flat=True)

                    sources = RelationshipAssociation.objects.filter(
                        destination_id__in=value,
                        source_type=self.relationship.source_type,
                        relationship=self.relationship,
                    ).values_list("source_id", flat=True)

                    values = list(destinations) + list(sources)

                # ModelMultipleChoiceFilters always have `distinct=True` so we must make sure that the
                # unioned queryset is also distinct. We also need to conditionally check if the incoming
                # `qs` is distinct in the case that a caller is manually passing in a queryset that may
                # not be distinct. (Ref: https://github.com/nautobot/nautobot/issues/2963)
                union_qs = self.get_method(self.qs)(Q(id__in=values))
                if qs.query.distinct:
                    union_qs = union_qs.distinct()

                return qs & union_qs

## `nautobot.apps.filters.RelationshipModelFilterSetMixin`

Bases: `django_filters.FilterSet`

Filterset for relationships applicable to the parent model.

Source code in `nautobot/extras/filters/mixins.py`

    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    class RelationshipModelFilterSetMixin(django_filters.FilterSet):
        """
        Filterset for relationships applicable to the parent model.
        """

        def __init__(self, *args, **kwargs):
            self.obj_type = ContentType.objects.get_for_model(self._meta.model)
            super().__init__(*args, **kwargs)
            self.relationships = []
            self._append_relationships(model=self._meta.model)

        def _append_relationships(self, model):
            """
            Append form fields for all Relationships assigned to this model.
            """
            src_relationships, dst_relationships = Relationship.objects.get_for_model(model=model, hidden=False)

            for rel in src_relationships:
                self._append_relationships_side([rel], RelationshipSideChoices.SIDE_SOURCE, model)

            for rel in dst_relationships:
                self._append_relationships_side([rel], RelationshipSideChoices.SIDE_DESTINATION, model)

        def _append_relationships_side(self, relationships, initial_side, model):
            """
            Helper method to _append_relationships, for processing one "side" of the relationships for this model.
            """
            for relationship in relationships:
                if relationship.symmetric:
                    side = RelationshipSideChoices.SIDE_PEER
                else:
                    side = initial_side
                peer_side = RelationshipSideChoices.OPPOSITE[side]

                # If this model is on the "source" side of the relationship, then the field will be named
                # "cr_<relationship_key>__destination" since it's used to pick the destination object(s).
                # If we're on the "destination" side, the field will be "cr_<relationship_key>__source".
                # For a symmetric relationship, both sides are "peer", so the field will be "cr_<relationship_key>__peer"
                field_name = f"cr_{relationship.key}__{peer_side}"

                if field_name in self.relationships:
                    # This is a symmetric relationship that we already processed from the opposing "initial_side".
                    # No need to process it a second time!
                    continue
                if peer_side == "source":
                    choice_model = relationship.source_type.model_class()
                elif peer_side == "destination":
                    choice_model = relationship.destination_type.model_class()
                else:
                    choice_model = model
                # Check for invalid_relationship unit test
                if choice_model:
                    self.filters[field_name] = RelationshipFilter(
                        relationship=relationship,
                        side=side,
                        field_name=field_name,
                        queryset=choice_model.objects.all(),
                        qs=model.objects.all(),
                    )
                self.relationships.append(field_name)

## `nautobot.apps.filters.RoleFilter`

Bases: `NaturalKeyOrPKMultipleChoiceFilter`

Filter field used for filtering Role fields.

Source code in `nautobot/extras/filters/mixins.py`

    287
    288
    289
    290
    291
    292
    293
    class RoleFilter(NaturalKeyOrPKMultipleChoiceFilter):
        """Filter field used for filtering Role fields."""

        def __init__(self, *args, **kwargs):
            kwargs.setdefault("queryset", Role.objects.all())
            kwargs.setdefault("label", "Role (name or ID)")
            super().__init__(*args, **kwargs)

## `nautobot.apps.filters.RoleModelFilterSetMixin`

Bases: `django_filters.FilterSet`

Mixin to add a `role` filter field to a FilterSet.

Source code in `nautobot/extras/filters/mixins.py`

    296
    297
    298
    299
    300
    301
    class RoleModelFilterSetMixin(django_filters.FilterSet):
        """
        Mixin to add a `role` filter field to a FilterSet.
        """

        role = RoleFilter()

## `nautobot.apps.filters.SearchFilter`

Bases: `MappedPredicatesFilterMixin`, `django_filters.CharFilter`

Provide a search filter for use on filtersets as the `q=` parameter.

See the docstring for `nautobot.core.filters.MappedPredicatesFilterMixin` for usage.

Source code in `nautobot/core/filters.py`

    442
    443
    444
    445
    446
    447
    448
    449
    class SearchFilter(MappedPredicatesFilterMixin, django_filters.CharFilter):
        """
        Provide a search filter for use on filtersets as the `q=` parameter.

        See the docstring for `nautobot.core.filters.MappedPredicatesFilterMixin` for usage.
        """

        label = "Search"

## `nautobot.apps.filters.StatusFilter`

Bases: `NaturalKeyOrPKMultipleChoiceFilter`

Filter field used for filtering Status fields.

Source code in `nautobot/extras/filters/mixins.py`

    304
    305
    306
    307
    308
    309
    310
    311
    312
    class StatusFilter(NaturalKeyOrPKMultipleChoiceFilter):
        """
        Filter field used for filtering Status fields.
        """

        def __init__(self, *args, **kwargs):
            kwargs.setdefault("queryset", Status.objects.all())
            kwargs.setdefault("label", "Status (name or ID)")
            super().__init__(*args, **kwargs)

## `nautobot.apps.filters.StatusModelFilterSetMixin`

Bases: `django_filters.FilterSet`

Mixin to add a `status` filter field to a FilterSet.

Source code in `nautobot/extras/filters/mixins.py`

    315
    316
    317
    318
    319
    320
    class StatusModelFilterSetMixin(django_filters.FilterSet):
        """
        Mixin to add a `status` filter field to a FilterSet.
        """

        status = StatusFilter()

## `nautobot.apps.filters.TagFilter`

Bases: `NaturalKeyOrPKMultipleChoiceFilter`

Match on one or more assigned tags. If multiple tags are specified (e.g. ?tag=foo&tag=bar), the queryset is filtered to objects matching all tags.

Source code in `nautobot/core/filters.py`

    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    class TagFilter(NaturalKeyOrPKMultipleChoiceFilter):
        """
        Match on one or more assigned tags. If multiple tags are specified (e.g. ?tag=foo&tag=bar), the queryset is filtered
        to objects matching all tags.
        """

        def __init__(self, *args, **kwargs):
            from nautobot.extras.models import Tag  # avoid circular import

            kwargs.setdefault("field_name", "tags")
            kwargs.setdefault("conjoined", True)
            kwargs.setdefault("label", "Tags")
            kwargs.setdefault("queryset", Tag.objects.all())

            super().__init__(*args, **kwargs)

### `__init__(*args, **kwargs)`

Source code in `nautobot/core/filters.py`

    458
    459
    460
    461
    462
    463
    464
    465
    466
    def __init__(self, *args, **kwargs):
        from nautobot.extras.models import Tag  # avoid circular import

        kwargs.setdefault("field_name", "tags")
        kwargs.setdefault("conjoined", True)
        kwargs.setdefault("label", "Tags")
        kwargs.setdefault("queryset", Tag.objects.all())

        super().__init__(*args, **kwargs)

## `nautobot.apps.filters.TenancyModelFilterSetMixin`

Bases: `django_filters.FilterSet`

An inheritable FilterSet for models which support Tenant assignment.

Source code in `nautobot/tenancy/filters/mixins.py`

     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class TenancyModelFilterSetMixin(django_filters.FilterSet):
        """
        An inheritable FilterSet for models which support Tenant assignment.
        """

        tenant_group = TreeNodeMultipleChoiceFilter(
            queryset=TenantGroup.objects.all(),
            field_name="tenant__tenant_group",
            to_field_name="name",
            label="Tenant Group (name or ID)",
        )
        tenant_id = django_filters.ModelMultipleChoiceFilter(
            queryset=Tenant.objects.all(),
            label='Tenant (ID) (deprecated, use "tenant" filter instead)',
        )
        tenant = NaturalKeyOrPKMultipleChoiceFilter(
            queryset=Tenant.objects.all(),
            to_field_name="name",
            label="Tenant (name or ID)",
        )

## `nautobot.apps.filters.TreeNodeMultipleChoiceFilter`

Bases: `NaturalKeyOrPKMultipleChoiceFilter`

Filter that matches on the given model(s) (identified by name and/or pk) as well as their tree descendants.

For example, if we have:

    Location "Earth"
      Location "USA"
        Location "GA" <- Location "Athens"
        Location "NC" <- Location "Durham"

a NaturalKeyOrPKMultipleChoiceFilter on Location for {"parent": "USA"} would only return "GA" and "NC" since that is the only two locations that have an immediate parent "USA" but a TreeNodeMultipleChoiceFilter on Location for {"parent": "USA"} would match both "Athens" and "Durham" in addition to "GA" and "NC".

Source code in `nautobot/core/filters.py`

    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    class TreeNodeMultipleChoiceFilter(NaturalKeyOrPKMultipleChoiceFilter):
        """
        Filter that matches on the given model(s) (identified by name and/or pk) _as well as their tree descendants._

        For example, if we have:

            Location "Earth"
              Location "USA"
                Location "GA" <- Location "Athens"
                Location "NC" <- Location "Durham"

        a NaturalKeyOrPKMultipleChoiceFilter on Location for {"parent": "USA"} would only return "GA" and "NC"
        since that is the only two locations that have an immediate parent "USA"
        but a TreeNodeMultipleChoiceFilter on Location for {"parent": "USA"}
        would match both "Athens" and "Durham" in addition to "GA" and "NC".
        """

        def __init__(self, *args, **kwargs):
            kwargs.pop("lookup_expr", None)  # Disallow overloading of `lookup_expr`.
            super().__init__(*args, **kwargs)

        def generate_query(self, value, qs=None, **kwargs):
            """
            Given a filter value, return a `Q` object that accounts for nested tree node descendants.
            """
            if value:
                # django-tree-queries
                value = [node.descendants(include_self=True) if not isinstance(node, str) else node for node in value]

            # This new_value is going to be a list of querysets that needs to be flattened.
            value = list(data_utils.flatten_iterable(value))

            # Construct a list of filter predicates that will be used to generate the Q object.
            predicates = []
            for obj in value:
                # Get the exact instance by PK as we are nested from the original query,
                #   or just pass the object through, commonly the null case.
                val = getattr(obj, "pk", obj)
                if val == self.null_value:
                    val = None
                predicates.append(self.get_filter_predicate(val))

            # Construct a nested OR query from the list of filter predicates derived from the flattened
            # listed of descendant objects.
            query = models.Q()
            for predicate in predicates:
                query |= models.Q(**predicate)

            return query

        def filter(self, qs, value):
            if value in EMPTY_VALUES:
                return qs

            # Fetch the generated Q object and filter the incoming qs with it before passing it along.
            query = self.generate_query(value)
            return self.get_method(qs)(query)

### `generate_query(value, qs=None, **kwargs)`

Given a filter value, return a `Q` object that accounts for nested tree node descendants.

Source code in `nautobot/core/filters.py`

    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    def generate_query(self, value, qs=None, **kwargs):
        """
        Given a filter value, return a `Q` object that accounts for nested tree node descendants.
        """
        if value:
            # django-tree-queries
            value = [node.descendants(include_self=True) if not isinstance(node, str) else node for node in value]

        # This new_value is going to be a list of querysets that needs to be flattened.
        value = list(data_utils.flatten_iterable(value))

        # Construct a list of filter predicates that will be used to generate the Q object.
        predicates = []
        for obj in value:
            # Get the exact instance by PK as we are nested from the original query,
            #   or just pass the object through, commonly the null case.
            val = getattr(obj, "pk", obj)
            if val == self.null_value:
                val = None
            predicates.append(self.get_filter_predicate(val))

        # Construct a nested OR query from the list of filter predicates derived from the flattened
        # listed of descendant objects.
        query = models.Q()
        for predicate in predicates:
            query |= models.Q(**predicate)

        return query

## `nautobot.apps.filters.multivalue_field_factory(field_class, widget=django_forms.SelectMultiple)`

Given a form field class, return a subclass capable of accepting multiple values. This allows us to OR on multiple filter values while maintaining the field's built-in validation. Example: GET /api/dcim/devices/?name=foo&name=bar

Source code in `nautobot/core/filters.py`

    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    def multivalue_field_factory(field_class, widget=django_forms.SelectMultiple):
        """
        Given a form field class, return a subclass capable of accepting multiple values. This allows us to OR on multiple
        filter values while maintaining the field's built-in validation. Example: GET /api/dcim/devices/?name=foo&name=bar
        """

        def to_python(self, value):
            if not value:
                return []

            # Make it a list if it's a string.
            if isinstance(value, (str, int)):
                value = [value]

            return [
                # Only append non-empty values (this avoids e.g. trying to cast '' as an integer)
                field_class.to_python(self, v)
                for v in value
                if v
            ]

        def validate(self, value):
            for v in value:
                field_class.validate(self, v)

        def run_validators(self, value):
            for v in value:
                field_class.run_validators(self, v)

        return type(
            f"MultiValue{field_class.__name__}",
            (field_class,),
            {
                "run_validators": run_validators,
                "to_python": to_python,
                "validate": validate,
                "widget": widget,
            },
        )