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/utilities/filters.py`

    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
    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": MultiValueNumberFilter},
                models.EmailField: {"filter_class": MultiValueCharFilter},
                models.FloatField: {"filter_class": MultiValueNumberFilter},
                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},
                MACAddressCharField: {"filter_class": MultiValueMACAddressFilter},
                TaggableManager: {"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,
                    MultiValueNumberFilter,
                    MultiValueTimeFilter,
                ),
            ):
                lookup_map = 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 = FILTER_NEGATION_LOOKUP_MAP

            # These filter types support only negation
            elif existing_filter.extra.get("choices"):
                lookup_map = FILTER_NEGATION_LOOKUP_MAP

            elif isinstance(
                existing_filter,
                (
                    django_filters.filters.CharFilter,
                    django_filters.MultipleChoiceFilter,
                    MultiValueCharFilter,
                    MultiValueMACAddressFilter,
                ),
            ):
                lookup_map = 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

            # 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:
                        # The filter field has been explicity 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
                        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 behaviour
                        # 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
            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()

            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

        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/utilities/filters.py`

    679
    680
    681
    682
    683
    684
    685
    686
    687
    688
    689
    690
    691
    692
    693
    694
    695
    696
    697
    @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
        cls.base_filters.update(
            cls._generate_lookup_expression_filters(filter_name=new_filter_name, filter_field=new_filter_field)
        )

### `get_filters()` `classmethod`

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

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

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

        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/utilities/filters.py`

    728
    729
    730
    731
    732
    733
    734
    735
    736
    737
    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.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`

     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
     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
    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,
            }

            custom_fields = CustomField.objects.filter(
                content_types=ContentType.objects.get_for_model(self._meta.model)
            ).exclude(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED)
            for cf in custom_fields:
                # Determine filter class for this CustomField type, default to CustomFieldBaseFilter
                # 2.0 TODO: #824 use cf.slug instead
                new_filter_name = f"cf_{cf.name}"
                filter_class = custom_field_filter_classes.get(cf.type, CustomFieldCharFilter)
                new_filter_field = filter_class(field_name=cf.name, custom_field=cf)
                new_filter_field.label = f"{cf.label}"

                # Create base filter (cf_customfieldname)
                self.filters[new_filter_name] = new_filter_field

                # 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)):
                lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP
            else:
                lookup_map = FILTER_CHAR_BASED_LOOKUP_MAP

            return lookup_map

        # TODO 2.0: Transition CustomField filters to nautobot.utilities.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.utilities.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_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.name,
                    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`

    450
    451
    452
    453
    454
    455
    456
    457
    class FilterExtension:
        """Class that may be returned by a registered Filter Extension function."""

        model = None

        filterset_fields = {}

        filterform_fields = {}

## `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 `slug`).

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

    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
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    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 `slug`).
        """

        field_class = MultiMatchModelMultipleChoiceField

        def __init__(self, *args, **kwargs):
            self.natural_key = kwargs.setdefault("to_field_name", "slug")
            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 slug and the filter predicate needs to be nested (e.g.
            # `{"site__slug": "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/utilities/filters.py`

    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    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 slug and the filter predicate needs to be nested (e.g.
        # `{"site__slug": "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`

    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    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.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/utilities/filters.py`

    120
    121
    122
    123
    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
    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,
                **kwargs,
            )

## `nautobot.apps.filters.RelationshipModelFilterSetMixin`

Bases: `django_filters.FilterSet`

Filterset for relationships applicable to the parent model.

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

    207
    208
    209
    210
    211
    212
    213
    214
    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
    264
    265
    266
    267
    268
    269
    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.
            """
            query = Q(source_type=self.obj_type, source_hidden=False) | Q(
                destination_type=self.obj_type, destination_hidden=False
            )
            relationships = Relationship.objects.select_related("source_type", "destination_type").filter(query)

            for rel in relationships.iterator():
                if rel.source_type == self.obj_type and not rel.source_hidden:
                    self._append_relationships_side([rel], RelationshipSideChoices.SIDE_SOURCE, model)
                if rel.destination_type == self.obj_type and not rel.destination_hidden:
                    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-slug>__destination" since it's used to pick the destination object(s).
                # If we're on the "destination" side, the field will be "cr_<relationship-slug>__source".
                # For a symmetric relationship, both sides are "peer", so the field will be "cr_<relationship-slug>__peer"
                field_name = f"cr_{relationship.slug}__{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.SearchFilter`

Bases: `MappedPredicatesFilterMixin`, `django_filters.CharFilter`

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

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

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

    463
    464
    465
    466
    467
    468
    469
    470
    class SearchFilter(MappedPredicatesFilterMixin, django_filters.CharFilter):
        """
        Provide a search filter for use on filtersets as the `q=` parameter.

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

        label = "Search"

## `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`

    302
    303
    304
    305
    306
    307
    class StatusModelFilterSetMixin(django_filters.FilterSet):
        """
        Mixin to add a `status` filter field to a FilterSet.
        """

        status = StatusFilter()

## `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
    27
    28
    29
    30
    class TenancyModelFilterSetMixin(django_filters.FilterSet):
        """
        An inheritable FilterSet for models which support Tenant assignment.
        """

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

## `nautobot.apps.filters.TreeNodeMultipleChoiceFilter`

Bases: `NaturalKeyOrPKMultipleChoiceFilter`

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

For example, if we have:

    Region "Earth"
      Region "USA"
        Region "GA" <- Site "Athens"
        Region "NC" <- Site "Durham"

a NaturalKeyOrPKMultipleChoiceFilter on Site for {"region": "USA"} would have no matches, since there are no Sites whose immediate Region is "USA", but a TreeNodeMultipleChoiceFilter on Site for {"region": "USA"} or {"region": "Earth"} would match both "Athens" and "Durham".

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

    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
    526
    527
    528
    529
    530
    531
    532
    533
    534
    class TreeNodeMultipleChoiceFilter(NaturalKeyOrPKMultipleChoiceFilter):
        """
        Filter that matches on the given model(s) (identified by slug and/or pk) _as well as their tree descendants._

        For example, if we have:

            Region "Earth"
              Region "USA"
                Region "GA" <- Site "Athens"
                Region "NC" <- Site "Durham"

        a NaturalKeyOrPKMultipleChoiceFilter on Site for {"region": "USA"} would have no matches,
        since there are no Sites whose immediate Region is "USA",
        but a TreeNodeMultipleChoiceFilter on Site for {"region": "USA"} or {"region": "Earth"}
        would match both "Athens" and "Durham".
        """

        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:
                if any(isinstance(node, TreeNode) for node in value):
                    # django-tree-queries
                    value = [node.descendants(include_self=True) if not isinstance(node, str) else node for node in value]
                elif any(isinstance(node, MPTTModel) for node in value):
                    # django-mptt
                    value = [
                        node.get_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(flatten_iterable(value))

            # Construct a list of filter predicates that will be used to generate the Q object.
            predicates = []
            for obj in value:
                # Try to get the `to_field_name` (e.g. `slug`) or just pass the object through.
                val = getattr(obj, self.field.to_field_name, 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/utilities/filters.py`

    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
    526
    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:
            if any(isinstance(node, TreeNode) for node in value):
                # django-tree-queries
                value = [node.descendants(include_self=True) if not isinstance(node, str) else node for node in value]
            elif any(isinstance(node, MPTTModel) for node in value):
                # django-mptt
                value = [
                    node.get_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(flatten_iterable(value))

        # Construct a list of filter predicates that will be used to generate the Q object.
        predicates = []
        for obj in value:
            # Try to get the `to_field_name` (e.g. `slug`) or just pass the object through.
            val = getattr(obj, self.field.to_field_name, 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