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
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
@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
@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
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
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
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
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
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
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
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
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.
        """
        source_relationships = Relationship.objects.filter(source_type=self.obj_type, source_hidden=False)
        self._append_relationships_side(source_relationships, RelationshipSideChoices.SIDE_SOURCE, model)

        dest_relationships = Relationship.objects.filter(destination_type=self.obj_type, destination_hidden=False)
        self._append_relationships_side(dest_relationships, 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
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
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
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
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
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