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

        # 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
        # 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
@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
@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
@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
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
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
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
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
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
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
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
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.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 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
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
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
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
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
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
@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
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
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
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
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
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
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_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

Limit role choices to the available role choices for self.model

Source code in nautobot/extras/filters/mixins.py
class RoleFilter(NaturalKeyOrPKMultipleChoiceFilter):
    """Limit role choices to the available role choices for self.model"""

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

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

    def get_queryset(self, request):
        return self.queryset.get_for_model(self.model)

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
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
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: django_filters.ModelMultipleChoiceFilter

Filter field used for filtering Status fields.

Explicitly sets to_field_name='value' and dynamically sets queryset to retrieve choices for the corresponding model & field name bound to the filterset.

Source code in nautobot/extras/filters/mixins.py
class StatusFilter(django_filters.ModelMultipleChoiceFilter):
    """
    Filter field used for filtering Status fields.

    Explicitly sets `to_field_name='value'` and dynamically sets queryset to
    retrieve choices for the corresponding model & field name bound to the
    filterset.
    """

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

    def get_queryset(self, request):
        self.queryset = Status.objects.all()
        return super().get_queryset(request)

    def get_filter_predicate(self, value):
        """Always use the field's name and the `to_field_name` attribute as predicate."""
        # e.g. `status__name`
        to_field_name = self.field.to_field_name
        name = f"{self.field_name}__{to_field_name}"
        # Sometimes the incoming value is an instance. This block of logic comes from the base
        # `get_filter_predicate()` and was added here to support this.
        try:
            return {name: getattr(value, to_field_name)}
        except (AttributeError, TypeError):
            return {name: value}

get_filter_predicate(value)

Always use the field's name and the to_field_name attribute as predicate.

Source code in nautobot/extras/filters/mixins.py
def get_filter_predicate(self, value):
    """Always use the field's name and the `to_field_name` attribute as predicate."""
    # e.g. `status__name`
    to_field_name = self.field.to_field_name
    name = f"{self.field_name}__{to_field_name}"
    # Sometimes the incoming value is an instance. This block of logic comes from the base
    # `get_filter_predicate()` and was added here to support this.
    try:
        return {name: getattr(value, to_field_name)}
    except (AttributeError, TypeError):
        return {name: value}

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