Skip to content

# `nautobot.apps.models`

Data model classes and utilities for app implementation.

## `nautobot.apps.models.AutoSlugField`

Bases: `_AutoSlugField`

AutoSlugField

By default, sets editable=True, blank=True, max_length=100, overwrite_on_add=False, unique=True Required arguments: populate_from Specifies which field, list of fields, or model method the slug will be populated from.

    populate_from can traverse a ForeignKey relationship
    by using Django ORM syntax:
        populate_from = 'related_model__field'

Optional arguments:

separator Defines the used separator (default: '-')

overwrite If set to True, overwrites the slug on every save (default: False)

overwrite_on_add If set to True, overwrites the provided slug on initial creation (default: False)

slugify_function Defines the function which will be used to "slugify" a content (default: :py:func:`~django.template.defaultfilters.slugify` )

It is possible to provide custom "slugify" function with the `slugify_function` function in a model class.

`slugify_function` function in a model class takes priority over `slugify_function` given as an argument to :py:class:`~AutoSlugField`.

Example

.. code-block:: python # models.py

    from django.db import models
    from django_extensions.db.fields import AutoSlugField

    class MyModel(models.Model):
        def slugify_function(self, content):
            return content.replace('_', '-').lower()

        title = models.CharField(max_length=42)
        slug = AutoSlugField(populate_from='title')

Taken from django_extensions AutoSlugField Documentation.

Source code in `nautobot/core/models/fields.py`

     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    class AutoSlugField(_AutoSlugField):
        """AutoSlugField

        By default, sets editable=True, blank=True, max_length=100, overwrite_on_add=False, unique=True
        Required arguments:
        populate_from
            Specifies which field, list of fields, or model method
            the slug will be populated from.

            populate_from can traverse a ForeignKey relationship
            by using Django ORM syntax:
                populate_from = 'related_model__field'

        Optional arguments:

        separator
            Defines the used separator (default: '-')

        overwrite
            If set to True, overwrites the slug on every save (default: False)

        overwrite_on_add
            If set to True, overwrites the provided slug on initial creation (default: False)

        slugify_function
            Defines the function which will be used to "slugify" a content
            (default: :py:func:`~django.template.defaultfilters.slugify` )

        It is possible to provide custom "slugify" function with
        the ``slugify_function`` function in a model class.

        ``slugify_function`` function in a model class takes priority over
        ``slugify_function`` given as an argument to :py:class:`~AutoSlugField`.

        Example

        .. code-block:: python
            # models.py

            from django.db import models
            from django_extensions.db.fields import AutoSlugField

            class MyModel(models.Model):
                def slugify_function(self, content):
                    return content.replace('_', '-').lower()

                title = models.CharField(max_length=42)
                slug = AutoSlugField(populate_from='title')

        Taken from django_extensions AutoSlugField Documentation.
        """

        def __init__(self, *args, **kwargs):
            kwargs.setdefault("max_length", 100)
            kwargs.setdefault("editable", True)
            kwargs.setdefault("overwrite_on_add", False)
            kwargs.setdefault("unique", True)
            super().__init__(*args, **kwargs)

        def get_slug_fields(self, model_instance, lookup_value):
            """Workaround for https://github.com/django-extensions/django-extensions/issues/1713."""
            try:
                return super().get_slug_fields(model_instance, lookup_value)
            except AttributeError:
                return ""

### `get_slug_fields(model_instance, lookup_value)`

Workaround for https://github.com/django-extensions/django-extensions/issues/1713.

Source code in `nautobot/core/models/fields.py`

    142
    143
    144
    145
    146
    147
    def get_slug_fields(self, model_instance, lookup_value):
        """Workaround for https://github.com/django-extensions/django-extensions/issues/1713."""
        try:
            return super().get_slug_fields(model_instance, lookup_value)
        except AttributeError:
            return ""

## `nautobot.apps.models.BaseManager`

Bases: `Manager`

Base manager class corresponding to BaseModel and RestrictedQuerySet.

Adds built-in natural key support, loosely based on `django-natural-keys`.

Source code in `nautobot/core/models/managers.py`

     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    class BaseManager(Manager):
        """
        Base manager class corresponding to BaseModel and RestrictedQuerySet.

        Adds built-in natural key support, loosely based on `django-natural-keys`.
        """

        def get_by_natural_key(self, *args):
            """
            Return the object corresponding to the provided natural key.

            Generic implementation that depends on the model being a BaseModel subclass or otherwise implementing our
            `natural_key_field_lookups` property API. Loosely based on implementation from `django-natural-keys`.
            """
            if len(args) == 1 and isinstance(args[0], (list, tuple)):
                logger.warning(
                    "%s.objects.get_by_natural_key() was called with a single %s as its args, "
                    "instead of a list of individual args. Did you forget a '*' in your call?",
                    self.model.__name__,
                    type(args[0]).__name__,
                )
                args = args[0]

            base_kwargs = self.model.natural_key_args_to_kwargs(args)

            # django-natural-keys had a pattern where it would replace nested related field lookups
            # (parent__namespace__name="Global", parent__prefix="10.0.0.0/8") with calls to get_by_natural_key()
            # (parent=Prefix.objects.get_by_natural_key("Global", "10.0.0.0/8")).
            # We initially followed this pattern, but it had the downside that an object's natural key could therefore
            # **only** reference related objects by their own natural keys, which is unnecessarily rigid.
            # We instead just do the simple thing and let Django follow the nested lookups as appropriate:
            return self.get(**base_kwargs)

### `get_by_natural_key(*args)`

Return the object corresponding to the provided natural key.

Generic implementation that depends on the model being a BaseModel subclass or otherwise implementing our `natural_key_field_lookups` property API. Loosely based on implementation from `django-natural-keys`.

Source code in `nautobot/core/models/managers.py`

    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    def get_by_natural_key(self, *args):
        """
        Return the object corresponding to the provided natural key.

        Generic implementation that depends on the model being a BaseModel subclass or otherwise implementing our
        `natural_key_field_lookups` property API. Loosely based on implementation from `django-natural-keys`.
        """
        if len(args) == 1 and isinstance(args[0], (list, tuple)):
            logger.warning(
                "%s.objects.get_by_natural_key() was called with a single %s as its args, "
                "instead of a list of individual args. Did you forget a '*' in your call?",
                self.model.__name__,
                type(args[0]).__name__,
            )
            args = args[0]

        base_kwargs = self.model.natural_key_args_to_kwargs(args)

        # django-natural-keys had a pattern where it would replace nested related field lookups
        # (parent__namespace__name="Global", parent__prefix="10.0.0.0/8") with calls to get_by_natural_key()
        # (parent=Prefix.objects.get_by_natural_key("Global", "10.0.0.0/8")).
        # We initially followed this pattern, but it had the downside that an object's natural key could therefore
        # **only** reference related objects by their own natural keys, which is unnecessarily rigid.
        # We instead just do the simple thing and let Django follow the nested lookups as appropriate:
        return self.get(**base_kwargs)

## `nautobot.apps.models.BaseModel`

Bases: `models.Model`

Base model class that all models should inherit from.

This abstract base provides globally common fields and functionality.

Here we define the primary key to be a UUID field and set its default to automatically generate a random UUID value. Note however, this does not operate in the same way as a traditional auto incrementing field for which the value is issued by the database upon initial insert. In the case of the UUID field, Django creates the value upon object instantiation. This means the canonical pattern in Django of checking `self.pk is None` to tell if an object has been created in the actual database does not work because the object will always have the value populated prior to being saved to the database for the first time. An alternate pattern of checking `not self.present_in_database` can be used for the same purpose in most cases.

Source code in `nautobot/core/models/__init__.py`

     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    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
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    class BaseModel(models.Model):
        """
        Base model class that all models should inherit from.

        This abstract base provides globally common fields and functionality.

        Here we define the primary key to be a UUID field and set its default to
        automatically generate a random UUID value. Note however, this does not
        operate in the same way as a traditional auto incrementing field for which
        the value is issued by the database upon initial insert. In the case of
        the UUID field, Django creates the value upon object instantiation. This
        means the canonical pattern in Django of checking `self.pk is None` to tell
        if an object has been created in the actual database does not work because
        the object will always have the value populated prior to being saved to the
        database for the first time. An alternate pattern of checking `not self.present_in_database`
        can be used for the same purpose in most cases.
        """

        id = models.UUIDField(primary_key=True, default=uuid.uuid4, unique=True, editable=False)

        objects = BaseManager.from_queryset(RestrictedQuerySet)()

        class Meta:
            abstract = True

        def get_absolute_url(self, api=False):
            """
            Return the canonical URL for this object in either the UI or the REST API.
            """

            # Iterate the pk-like fields and try to get a URL, or return None.
            fields = ["pk"]
            actions = ["retrieve", "detail", ""]  # TODO: Eventually all retrieve

            for field in fields:
                if not hasattr(self, field):
                    continue

                for action in actions:
                    route = get_route_for_model(self, action, api=api)

                    try:
                        return reverse(route, kwargs={field: getattr(self, field)})
                    except NoReverseMatch:
                        continue

            raise AttributeError(f"Cannot find a URL for {self} ({self._meta.app_label}.{self._meta.model_name})")

        @property
        def present_in_database(self):
            """
            True if the record exists in the database, False if it does not.
            """
            return not self._state.adding

        @classproperty  # https://github.com/PyCQA/pylint-django/issues/240
        def _content_type(cls):  # pylint: disable=no-self-argument
            """
            Return the ContentType of the object, never cached.
            """
            return ContentType.objects.get_for_model(cls)

        @classproperty  # https://github.com/PyCQA/pylint-django/issues/240
        def _content_type_cache_key(cls):  # pylint: disable=no-self-argument
            """
            Return the cache key for the ContentType of the object.

            Necessary for use with _content_type_cached and management commands.
            """
            return f"nautobot.{cls._meta.label_lower}._content_type"

        @classproperty  # https://github.com/PyCQA/pylint-django/issues/240
        def _content_type_cached(cls):  # pylint: disable=no-self-argument
            """
            Return the ContentType of the object, cached.
            """

            return cache.get_or_set(cls._content_type_cache_key, cls._content_type, settings.CONTENT_TYPE_CACHE_TIMEOUT)

        def validated_save(self, *args, **kwargs):
            """
            Perform model validation during instance save.

            This is a convenience method that first calls `self.full_clean()` and then `self.save()`
            which in effect enforces model validation prior to saving the instance, without having
            to manually make these calls seperately. This is a slight departure from Django norms,
            but is intended to offer an optional, simplified interface for performing this common
            workflow. The intended use is for user defined Jobs run via the `nautobot-server nbshell`
            command.
            """
            self.full_clean()
            self.save(*args, **kwargs)

        def natural_key(self) -> list:
            """
            Smarter default implementation of natural key construction.

            1. Handles nullable foreign keys (https://github.com/wq/django-natural-keys/issues/18)
            2. Handles variadic natural-keys (e.g. Location model - [name, parent__name, parent__parent__name, ...].)
            """
            vals = []
            for lookups in [lookup.split("__") for lookup in self.natural_key_field_lookups]:
                val = self
                for lookup in lookups:
                    val = getattr(val, lookup)
                    if val is None:
                        break
                if not is_protected_type(val):
                    val = str(val)
                vals.append(val)
            # Strip trailing Nones from vals
            while vals and vals[-1] is None:
                vals.pop()
            return vals

        @property
        def composite_key(self) -> str:
            """
            Automatic "slug" string derived from this model's natural key, suitable for use in URLs etc.

            A less naïve implementation than django-natural-keys provides by default, based around URL percent-encoding.
            """
            return construct_composite_key(self.natural_key())

        @property
        def natural_slug(self) -> str:
            """
            Automatic "slug" string derived from this model's natural key. This differs from composite
            key in that it must be human-readable and comply with a very limited character set, and is therefore lossy.
            This value is not guaranteed to be
            unique although a best effort is made by appending a fragment of the primary key to the
            natural slug value.
            """
            return construct_natural_slug(self.natural_key(), pk=self.pk)

        @classmethod
        def _generate_field_lookups_from_natural_key_field_names(cls, natural_key_field_names):
            """Generate field lookups based on natural key field names."""
            natural_key_field_lookups = []
            for field_name in natural_key_field_names:
                # field_name could be a related field that has its own natural key fields (`parent`),
                # *or* it could be an explicit set of traversals (`parent__namespace__name`). Handle both.
                model = cls
                for field_component in field_name.split("__")[:-1]:
                    model = model._meta.get_field(field_component).remote_field.model

                try:
                    field = model._meta.get_field(field_name.split("__")[-1])
                except FieldDoesNotExist:
                    # Not a database field, maybe it's a property instead?
                    if hasattr(model, field_name) and isinstance(getattr(model, field_name), property):
                        natural_key_field_lookups.append(field_name)
                        continue
                    raise

                if getattr(field, "remote_field", None) is None:
                    # Not a related field, so the field name is the field lookup
                    natural_key_field_lookups.append(field_name)
                    continue

                related_model = field.remote_field.model
                related_natural_key_field_lookups = None
                if hasattr(related_model, "natural_key_field_lookups"):
                    # TODO: generic handling for self-referential case, as seen in Location
                    related_natural_key_field_lookups = related_model.natural_key_field_lookups
                else:
                    # Related model isn't a Nautobot model and so doesn't have a `natural_key_field_lookups`.
                    # The common case we've encountered so far is the contenttypes.ContentType model:
                    if related_model._meta.app_label == "contenttypes" and related_model._meta.model_name == "contenttype":
                        related_natural_key_field_lookups = ["app_label", "model"]
                    # Additional special cases can be added here

                if not related_natural_key_field_lookups:
                    raise AttributeError(
                        f"Unable to determine the related natural-key fields for {related_model.__name__} "
                        f"(as referenced from {cls.__name__}.{field_name}). If the related model is a non-Nautobot "
                        "model (such as ContentType) then it may be appropriate to add special-case handling for this "
                        "model in BaseModel.natural_key_field_lookups; alternately you may be able to solve this for "
                        f"a single special case by explicitly defining {cls.__name__}.natural_key_field_lookups."
                    )

                for field_lookup in related_natural_key_field_lookups:
                    natural_key_field_lookups.append(f"{field_name}__{field_lookup}")

            return natural_key_field_lookups

        @classmethod
        def csv_natural_key_field_lookups(cls):
            """Override this method for models with Python `@property` as part of their `natural_key_field_names`.

            Since CSV export for `natural_key_field_names` relies on database fields, you can override this method
            to provide custom handling for models with property-based natural keys.
            """
            return cls.natural_key_field_lookups

        @classproperty  # https://github.com/PyCQA/pylint-django/issues/240
        def natural_key_field_lookups(cls):  # pylint: disable=no-self-argument
            """
            List of lookups (possibly including nested lookups for related models) that make up this model's natural key.

            BaseModel provides a "smart" implementation that tries to determine this automatically,
            but you can also explicitly set `natural_key_field_names` on a given model subclass if desired.

            This property is based on a consolidation of `django-natural-keys` `ForeignKeyModel.get_natural_key_info()`,
            `ForeignKeyModel.get_natural_key_def()`, and `ForeignKeyModel.get_natural_key_fields()`.

            Unlike `get_natural_key_def()`, this doesn't auto-exclude all AutoField and BigAutoField fields,
            but instead explicitly discounts the `id` field (only) as a candidate.
            """
            # First, figure out which local fields comprise the natural key:
            natural_key_field_names = []
            if hasattr(cls, "natural_key_field_names"):
                natural_key_field_names = cls.natural_key_field_names
            else:
                # Does this model have any new-style UniqueConstraints? If so, pick the first one
                for constraint in cls._meta.constraints:
                    if isinstance(constraint, models.UniqueConstraint):
                        natural_key_field_names = constraint.fields
                        break
                else:
                    # Else, does this model have any old-style unique_together? If so, pick the first one.
                    if cls._meta.unique_together:
                        natural_key_field_names = cls._meta.unique_together[0]
                    else:
                        # Else, do we have any individual unique=True fields? If so, pick the first one.
                        unique_fields = [field for field in cls._meta.fields if field.unique and field.name != "id"]
                        if unique_fields:
                            natural_key_field_names = (unique_fields[0].name,)

            if not natural_key_field_names:
                raise AttributeError(
                    f"Unable to identify an intrinsic natural-key definition for {cls.__name__}. "
                    "If there isn't at least one UniqueConstraint, unique_together, or field with unique=True, "
                    "you probably need to explicitly declare the 'natural_key_field_names' for this model, "
                    "or potentially override the default 'natural_key_field_lookups' implementation for this model."
                )

            # Next, for any natural key fields that have related models, get the natural key for the related model if known
            return cls._generate_field_lookups_from_natural_key_field_names(natural_key_field_names)

        @classmethod
        def natural_key_args_to_kwargs(cls, args):
            """
            Helper function to map a list of natural key field values to actual kwargs suitable for lookup and filtering.

            Based on `django-natural-keys` `NaturalKeyQuerySet.natural_key_kwargs()` method.
            """
            args = list(args)
            natural_key_field_lookups = cls.natural_key_field_lookups
            # Because `natural_key` strips trailing `None` from the natural key to handle the variadic-natural-key case,
            # we may need to add trailing `None` back on to make the number of args match back up.
            while len(args) < len(natural_key_field_lookups):
                args.append(None)
            # However, if we have *too many* args, that's just incorrect usage:
            if len(args) > len(natural_key_field_lookups):
                raise ValueError(
                    f"Wrong number of natural-key args for {cls.__name__}.natural_key_args_to_kwargs() -- "
                    f"expected no more than {len(natural_key_field_lookups)} but got {len(args)}."
                )
            return dict(zip(natural_key_field_lookups, args))

### `composite_key: str` `property`

Automatic "slug" string derived from this model's natural key, suitable for use in URLs etc.

A less naïve implementation than django-natural-keys provides by default, based around URL percent-encoding.

### `natural_slug: str` `property`

Automatic "slug" string derived from this model's natural key. This differs from composite key in that it must be human-readable and comply with a very limited character set, and is therefore lossy. This value is not guaranteed to be unique although a best effort is made by appending a fragment of the primary key to the natural slug value.

### `present_in_database` `property`

True if the record exists in the database, False if it does not.

### `csv_natural_key_field_lookups()` `classmethod`

Override this method for models with Python `@property` as part of their `natural_key_field_names`.

Since CSV export for `natural_key_field_names` relies on database fields, you can override this method to provide custom handling for models with property-based natural keys.

Source code in `nautobot/core/models/__init__.py`

    214
    215
    216
    217
    218
    219
    220
    221
    @classmethod
    def csv_natural_key_field_lookups(cls):
        """Override this method for models with Python `@property` as part of their `natural_key_field_names`.

        Since CSV export for `natural_key_field_names` relies on database fields, you can override this method
        to provide custom handling for models with property-based natural keys.
        """
        return cls.natural_key_field_lookups

### `get_absolute_url(api=False)`

Return the canonical URL for this object in either the UI or the REST API.

Source code in `nautobot/core/models/__init__.py`

    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    def get_absolute_url(self, api=False):
        """
        Return the canonical URL for this object in either the UI or the REST API.
        """

        # Iterate the pk-like fields and try to get a URL, or return None.
        fields = ["pk"]
        actions = ["retrieve", "detail", ""]  # TODO: Eventually all retrieve

        for field in fields:
            if not hasattr(self, field):
                continue

            for action in actions:
                route = get_route_for_model(self, action, api=api)

                try:
                    return reverse(route, kwargs={field: getattr(self, field)})
                except NoReverseMatch:
                    continue

        raise AttributeError(f"Cannot find a URL for {self} ({self._meta.app_label}.{self._meta.model_name})")

### `natural_key()`

Smarter default implementation of natural key construction.

1. Handles nullable foreign keys (https://github.com/wq/django-natural-keys/issues/18)
2. Handles variadic natural-keys (e.g. Location model - [name, parent__name, parent__parent__name, ...].)

Source code in `nautobot/core/models/__init__.py`

    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    def natural_key(self) -> list:
        """
        Smarter default implementation of natural key construction.

        1. Handles nullable foreign keys (https://github.com/wq/django-natural-keys/issues/18)
        2. Handles variadic natural-keys (e.g. Location model - [name, parent__name, parent__parent__name, ...].)
        """
        vals = []
        for lookups in [lookup.split("__") for lookup in self.natural_key_field_lookups]:
            val = self
            for lookup in lookups:
                val = getattr(val, lookup)
                if val is None:
                    break
            if not is_protected_type(val):
                val = str(val)
            vals.append(val)
        # Strip trailing Nones from vals
        while vals and vals[-1] is None:
            vals.pop()
        return vals

### `natural_key_args_to_kwargs(args)` `classmethod`

Helper function to map a list of natural key field values to actual kwargs suitable for lookup and filtering.

Based on `django-natural-keys` `NaturalKeyQuerySet.natural_key_kwargs()` method.

Source code in `nautobot/core/models/__init__.py`

    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    @classmethod
    def natural_key_args_to_kwargs(cls, args):
        """
        Helper function to map a list of natural key field values to actual kwargs suitable for lookup and filtering.

        Based on `django-natural-keys` `NaturalKeyQuerySet.natural_key_kwargs()` method.
        """
        args = list(args)
        natural_key_field_lookups = cls.natural_key_field_lookups
        # Because `natural_key` strips trailing `None` from the natural key to handle the variadic-natural-key case,
        # we may need to add trailing `None` back on to make the number of args match back up.
        while len(args) < len(natural_key_field_lookups):
            args.append(None)
        # However, if we have *too many* args, that's just incorrect usage:
        if len(args) > len(natural_key_field_lookups):
            raise ValueError(
                f"Wrong number of natural-key args for {cls.__name__}.natural_key_args_to_kwargs() -- "
                f"expected no more than {len(natural_key_field_lookups)} but got {len(args)}."
            )
        return dict(zip(natural_key_field_lookups, args))

### `natural_key_field_lookups()`

List of lookups (possibly including nested lookups for related models) that make up this model's natural key.

BaseModel provides a "smart" implementation that tries to determine this automatically, but you can also explicitly set `natural_key_field_names` on a given model subclass if desired.

This property is based on a consolidation of `django-natural-keys` `ForeignKeyModel.get_natural_key_info()`, `ForeignKeyModel.get_natural_key_def()`, and `ForeignKeyModel.get_natural_key_fields()`.

Unlike `get_natural_key_def()`, this doesn't auto-exclude all AutoField and BigAutoField fields, but instead explicitly discounts the `id` field (only) as a candidate.

Source code in `nautobot/core/models/__init__.py`

    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
    @classproperty  # https://github.com/PyCQA/pylint-django/issues/240
    def natural_key_field_lookups(cls):  # pylint: disable=no-self-argument
        """
        List of lookups (possibly including nested lookups for related models) that make up this model's natural key.

        BaseModel provides a "smart" implementation that tries to determine this automatically,
        but you can also explicitly set `natural_key_field_names` on a given model subclass if desired.

        This property is based on a consolidation of `django-natural-keys` `ForeignKeyModel.get_natural_key_info()`,
        `ForeignKeyModel.get_natural_key_def()`, and `ForeignKeyModel.get_natural_key_fields()`.

        Unlike `get_natural_key_def()`, this doesn't auto-exclude all AutoField and BigAutoField fields,
        but instead explicitly discounts the `id` field (only) as a candidate.
        """
        # First, figure out which local fields comprise the natural key:
        natural_key_field_names = []
        if hasattr(cls, "natural_key_field_names"):
            natural_key_field_names = cls.natural_key_field_names
        else:
            # Does this model have any new-style UniqueConstraints? If so, pick the first one
            for constraint in cls._meta.constraints:
                if isinstance(constraint, models.UniqueConstraint):
                    natural_key_field_names = constraint.fields
                    break
            else:
                # Else, does this model have any old-style unique_together? If so, pick the first one.
                if cls._meta.unique_together:
                    natural_key_field_names = cls._meta.unique_together[0]
                else:
                    # Else, do we have any individual unique=True fields? If so, pick the first one.
                    unique_fields = [field for field in cls._meta.fields if field.unique and field.name != "id"]
                    if unique_fields:
                        natural_key_field_names = (unique_fields[0].name,)

        if not natural_key_field_names:
            raise AttributeError(
                f"Unable to identify an intrinsic natural-key definition for {cls.__name__}. "
                "If there isn't at least one UniqueConstraint, unique_together, or field with unique=True, "
                "you probably need to explicitly declare the 'natural_key_field_names' for this model, "
                "or potentially override the default 'natural_key_field_lookups' implementation for this model."
            )

        # Next, for any natural key fields that have related models, get the natural key for the related model if known
        return cls._generate_field_lookups_from_natural_key_field_names(natural_key_field_names)

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

Perform model validation during instance save.

This is a convenience method that first calls `self.full_clean()` and then `self.save()` which in effect enforces model validation prior to saving the instance, without having to manually make these calls seperately. This is a slight departure from Django norms, but is intended to offer an optional, simplified interface for performing this common workflow. The intended use is for user defined Jobs run via the `nautobot-server nbshell` command.

Source code in `nautobot/core/models/__init__.py`

    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    def validated_save(self, *args, **kwargs):
        """
        Perform model validation during instance save.

        This is a convenience method that first calls `self.full_clean()` and then `self.save()`
        which in effect enforces model validation prior to saving the instance, without having
        to manually make these calls seperately. This is a slight departure from Django norms,
        but is intended to offer an optional, simplified interface for performing this common
        workflow. The intended use is for user defined Jobs run via the `nautobot-server nbshell`
        command.
        """
        self.full_clean()
        self.save(*args, **kwargs)

## `nautobot.apps.models.ChangeLoggedModel`

Bases: `models.Model`

An abstract model which adds fields to store the creation and last-updated times for an object. Both fields can be null to facilitate adding these fields to existing instances via a database migration.

Source code in `nautobot/extras/models/change_logging.py`

    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    class ChangeLoggedModel(models.Model):
        """
        An abstract model which adds fields to store the creation and last-updated times for an object. Both fields can be
        null to facilitate adding these fields to existing instances via a database migration.
        """

        created = models.DateTimeField(auto_now_add=True, blank=True, null=True)
        last_updated = models.DateTimeField(auto_now=True, blank=True, null=True)

        class Meta:
            abstract = True

        def to_objectchange(self, action, *, related_object=None, object_data_extra=None, object_data_exclude=None):
            """
            Return a new ObjectChange representing a change made to this object. This will typically be called automatically
            by ChangeLoggingMiddleware.
            """

            return ObjectChange(
                changed_object=self,
                object_repr=str(self)[:CHANGELOG_MAX_OBJECT_REPR],
                action=action,
                object_data=serialize_object(self, extra=object_data_extra, exclude=object_data_exclude),
                object_data_v2=serialize_object_v2(self),
                related_object=related_object,
            )

        def get_changelog_url(self):
            """Return the changelog URL for this object."""
            route = get_route_for_model(self, "changelog")

            # Iterate the pk-like fields and try to get a URL, or return None.
            fields = ["pk", "slug"]
            for field in fields:
                if not hasattr(self, field):
                    continue

                try:
                    return reverse(route, kwargs={field: getattr(self, field)})
                except NoReverseMatch:
                    continue

            return None

### `get_changelog_url()`

Return the changelog URL for this object.

Source code in `nautobot/extras/models/change_logging.py`

    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    def get_changelog_url(self):
        """Return the changelog URL for this object."""
        route = get_route_for_model(self, "changelog")

        # Iterate the pk-like fields and try to get a URL, or return None.
        fields = ["pk", "slug"]
        for field in fields:
            if not hasattr(self, field):
                continue

            try:
                return reverse(route, kwargs={field: getattr(self, field)})
            except NoReverseMatch:
                continue

        return None

### `to_objectchange(action, *, related_object=None, object_data_extra=None, object_data_exclude=None)`

Return a new ObjectChange representing a change made to this object. This will typically be called automatically by ChangeLoggingMiddleware.

Source code in `nautobot/extras/models/change_logging.py`

    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    def to_objectchange(self, action, *, related_object=None, object_data_extra=None, object_data_exclude=None):
        """
        Return a new ObjectChange representing a change made to this object. This will typically be called automatically
        by ChangeLoggingMiddleware.
        """

        return ObjectChange(
            changed_object=self,
            object_repr=str(self)[:CHANGELOG_MAX_OBJECT_REPR],
            action=action,
            object_data=serialize_object(self, extra=object_data_extra, exclude=object_data_exclude),
            object_data_v2=serialize_object_v2(self),
            related_object=related_object,
        )

## `nautobot.apps.models.CollateAsChar`

Bases: `Func`

Disregard localization by collating a field as a plain character string. Helpful for ensuring predictable ordering.

Source code in `nautobot/core/models/query_functions.py`

     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class CollateAsChar(Func):
        """
        Disregard localization by collating a field as a plain character string. Helpful for ensuring predictable ordering.
        """

        function = None
        template = "(%(expressions)s) COLLATE %(function)s"

        def as_sql(self, compiler, connection, function=None, template=None, arg_joiner=None, **extra_context):
            vendor = connection.vendor
            # Mapping of vendor => function
            func_map = {
                "postgresql": '"C"',
                "mysql": "utf8mb4_bin",
            }

            if vendor not in func_map:
                raise NotSupportedError(f"CollateAsChar is not supported for database {vendor}")

            function = func_map[connection.vendor]

            return super().as_sql(compiler, connection, function, template, arg_joiner, **extra_context)

## `nautobot.apps.models.CompositeKeyQuerySetMixin`

Mixin to extend a base queryset class with support for filtering by `composite_key=...` as a virtual parameter.

Example

> > > Location.objects.last().composite_key 'Durham;AMER'

Note that `Location.composite_key` is a `@property`, not a database field, and so would not normally be usable in a `QuerySet` query, but because `RestrictedQuerySet` inherits from this mixin, the following "just works":

    >>> Location.objects.get(composite_key="Durham;AMER")
    <Location: Durham>

This is a shorthand for what would otherwise be a multi-step process

> > > from nautobot.core.models.utils import deconstruct_composite_key deconstruct_composite_key("Durham;AMER") ['Durham', 'AMER'] Location.natural_key_args_to_kwargs(['Durham', 'AMER']) {'name': 'Durham', 'parent__name': 'AMER'} Location.objects.get(name="Durham", parent__name="AMER")

This works for QuerySet `filter()` and `exclude()` as well:

    >>> Location.objects.filter(composite_key='Durham;AMER')
    <LocationQuerySet [<Location: Durham>]>
    >>> Location.objects.exclude(composite_key='Durham;AMER')
    <LocationQuerySet [<Location: AMER>]>

`composite_key` can also be used in combination with other query parameters:

    >>> Location.objects.filter(composite_key='Durham;AMER', status__name='Planned')
    <LocationQuerySet []>

It will raise a ValueError if the deconstructed composite key collides with another query parameter

> > > Location.objects.filter(composite_key='Durham;AMER', name='Raleigh') ValueError: Conflicting values for key "name": ('Durham', 'Raleigh')

See also `BaseModel.composite_key` and `utils.construct_composite_key()`/`utils.deconstruct_composite_key()`.

Source code in `nautobot/core/models/querysets.py`

     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     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
    class CompositeKeyQuerySetMixin:
        """
        Mixin to extend a base queryset class with support for filtering by `composite_key=...` as a virtual parameter.

        Example:

            >>> Location.objects.last().composite_key
            'Durham;AMER'

        Note that `Location.composite_key` is a `@property`, *not* a database field, and so would not normally be usable in
        a `QuerySet` query, but because `RestrictedQuerySet` inherits from this mixin, the following "just works":

            >>> Location.objects.get(composite_key="Durham;AMER")
            <Location: Durham>

        This is a shorthand for what would otherwise be a multi-step process:

            >>> from nautobot.core.models.utils import deconstruct_composite_key
            >>> deconstruct_composite_key("Durham;AMER")
            ['Durham', 'AMER']
            >>> Location.natural_key_args_to_kwargs(['Durham', 'AMER'])
            {'name': 'Durham', 'parent__name': 'AMER'}
            >>> Location.objects.get(name="Durham", parent__name="AMER")
            <Location: Durham>

        This works for QuerySet `filter()` and `exclude()` as well:

            >>> Location.objects.filter(composite_key='Durham;AMER')
            <LocationQuerySet [<Location: Durham>]>
            >>> Location.objects.exclude(composite_key='Durham;AMER')
            <LocationQuerySet [<Location: AMER>]>

        `composite_key` can also be used in combination with other query parameters:

            >>> Location.objects.filter(composite_key='Durham;AMER', status__name='Planned')
            <LocationQuerySet []>

        It will raise a ValueError if the deconstructed composite key collides with another query parameter:

            >>> Location.objects.filter(composite_key='Durham;AMER', name='Raleigh')
            ValueError: Conflicting values for key "name": ('Durham', 'Raleigh')

        See also `BaseModel.composite_key` and `utils.construct_composite_key()`/`utils.deconstruct_composite_key()`.
        """

        def split_composite_key_into_kwargs(self, composite_key=None, **kwargs):
            """
            Helper method abstracting a common need from filter() and exclude().

            Subclasses may need to call this directly if they also have special processing of other filter/exclude params.
            """
            if composite_key and isinstance(composite_key, str):
                natural_key_values = deconstruct_composite_key(composite_key)
                return merge_dicts_without_collision(self.model.natural_key_args_to_kwargs(natural_key_values), kwargs)
            return kwargs

        def filter(self, *args, composite_key=None, **kwargs):
            """
            Explicitly handle `filter(composite_key="...")` by decomposing the composite-key into natural key parameters.

            Counterpart to BaseModel.composite_key property.
            """
            return super().filter(*args, **self.split_composite_key_into_kwargs(composite_key, **kwargs))

        def exclude(self, *args, composite_key=None, **kwargs):
            """
            Explicitly handle `exclude(composite_key="...")` by decomposing the composite-key into natural key parameters.

            Counterpart to BaseModel.composite_key property.
            """
            return super().exclude(*args, **self.split_composite_key_into_kwargs(composite_key, **kwargs))

### `exclude(*args, composite_key=None, **kwargs)`

Explicitly handle `exclude(composite_key="...")` by decomposing the composite-key into natural key parameters.

Counterpart to BaseModel.composite_key property.

Source code in `nautobot/core/models/querysets.py`

     99
    100
    101
    102
    103
    104
    105
    def exclude(self, *args, composite_key=None, **kwargs):
        """
        Explicitly handle `exclude(composite_key="...")` by decomposing the composite-key into natural key parameters.

        Counterpart to BaseModel.composite_key property.
        """
        return super().exclude(*args, **self.split_composite_key_into_kwargs(composite_key, **kwargs))

### `filter(*args, composite_key=None, **kwargs)`

Explicitly handle `filter(composite_key="...")` by decomposing the composite-key into natural key parameters.

Counterpart to BaseModel.composite_key property.

Source code in `nautobot/core/models/querysets.py`

    91
    92
    93
    94
    95
    96
    97
    def filter(self, *args, composite_key=None, **kwargs):
        """
        Explicitly handle `filter(composite_key="...")` by decomposing the composite-key into natural key parameters.

        Counterpart to BaseModel.composite_key property.
        """
        return super().filter(*args, **self.split_composite_key_into_kwargs(composite_key, **kwargs))

### `split_composite_key_into_kwargs(composite_key=None, **kwargs)`

Helper method abstracting a common need from filter() and exclude().

Subclasses may need to call this directly if they also have special processing of other filter/exclude params.

Source code in `nautobot/core/models/querysets.py`

    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    def split_composite_key_into_kwargs(self, composite_key=None, **kwargs):
        """
        Helper method abstracting a common need from filter() and exclude().

        Subclasses may need to call this directly if they also have special processing of other filter/exclude params.
        """
        if composite_key and isinstance(composite_key, str):
            natural_key_values = deconstruct_composite_key(composite_key)
            return merge_dicts_without_collision(self.model.natural_key_args_to_kwargs(natural_key_values), kwargs)
        return kwargs

## `nautobot.apps.models.ConfigContextModel`

Bases: `models.Model`, `ConfigContextSchemaValidationMixin`

A model which includes local configuration context data. This local data will override any inherited data from ConfigContexts.

Source code in `nautobot/extras/models/models.py`

    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    class ConfigContextModel(models.Model, ConfigContextSchemaValidationMixin):
        """
        A model which includes local configuration context data. This local data will override any inherited data from
        ConfigContexts.
        """

        local_config_context_data = models.JSONField(
            encoder=DjangoJSONEncoder,
            blank=True,
            null=True,
        )
        local_config_context_schema = ForeignKeyWithAutoRelatedName(
            to="extras.ConfigContextSchema",
            on_delete=models.SET_NULL,
            null=True,
            blank=True,
            help_text="Optional schema to validate the structure of the data",
        )
        # The local context data *may* be owned by another model, such as a GitRepository, or it may be un-owned
        local_config_context_data_owner_content_type = ForeignKeyWithAutoRelatedName(
            to=ContentType,
            on_delete=models.CASCADE,
            limit_choices_to=FeatureQuery("config_context_owners"),
            default=None,
            null=True,
            blank=True,
        )
        local_config_context_data_owner_object_id = models.UUIDField(default=None, null=True, blank=True)
        local_config_context_data_owner = GenericForeignKey(
            ct_field="local_config_context_data_owner_content_type",
            fk_field="local_config_context_data_owner_object_id",
        )

        class Meta:
            abstract = True
            indexes = [
                models.Index(
                    fields=("local_config_context_data_owner_content_type", "local_config_context_data_owner_object_id")
                ),
            ]

        def get_config_context(self):
            """
            Return the rendered configuration context for a device or VM.
            """
            if not hasattr(self, "config_context_data"):
                # Annotation not available, so fall back to manually querying for the config context
                config_context_data = ConfigContext.objects.get_for_object(self).values_list("data", flat=True)
            else:
                config_context_data = self.config_context_data or []
                config_context_data = [
                    c["data"] for c in sorted(config_context_data, key=lambda k: (k["weight"], k["name"]))
                ]
            # Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
            data = OrderedDict()
            for context in config_context_data:
                data = deepmerge(data, context)

            # If the object has local config context data defined, merge it last
            if self.local_config_context_data:
                data = deepmerge(data, self.local_config_context_data)

            return data

        def clean(self):
            super().clean()

            # Verify that JSON data is provided as an object
            if self.local_config_context_data and not isinstance(self.local_config_context_data, dict):
                raise ValidationError(
                    {"local_config_context_data": 'JSON data must be in object form. Example: {"foo": 123}'}
                )

            if self.local_config_context_schema and not self.local_config_context_data:
                raise ValidationError(
                    {"local_config_context_schema": "Local config context data must exist for a schema to be applied."}
                )

            # Validate data against schema
            self._validate_with_schema("local_config_context_data", "local_config_context_schema")

### `get_config_context()`

Return the rendered configuration context for a device or VM.

Source code in `nautobot/extras/models/models.py`

    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    def get_config_context(self):
        """
        Return the rendered configuration context for a device or VM.
        """
        if not hasattr(self, "config_context_data"):
            # Annotation not available, so fall back to manually querying for the config context
            config_context_data = ConfigContext.objects.get_for_object(self).values_list("data", flat=True)
        else:
            config_context_data = self.config_context_data or []
            config_context_data = [
                c["data"] for c in sorted(config_context_data, key=lambda k: (k["weight"], k["name"]))
            ]
        # Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
        data = OrderedDict()
        for context in config_context_data:
            data = deepmerge(data, context)

        # If the object has local config context data defined, merge it last
        if self.local_config_context_data:
            data = deepmerge(data, self.local_config_context_data)

        return data

## `nautobot.apps.models.ConfigContextSchemaValidationMixin`

Mixin that provides validation of config context data against a json schema.

Source code in `nautobot/extras/models/models.py`

    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    class ConfigContextSchemaValidationMixin:
        """
        Mixin that provides validation of config context data against a json schema.
        """

        def _validate_with_schema(self, data_field, schema_field):
            schema = getattr(self, schema_field)
            data = getattr(self, data_field)

            # If schema is None, then no schema has been specified on the instance and thus no validation should occur.
            if schema:
                try:
                    Draft7Validator(schema.data_schema, format_checker=Draft7Validator.FORMAT_CHECKER).validate(data)
                except JSONSchemaValidationError as e:
                    raise ValidationError({data_field: [f"Validation using the JSON Schema {schema} failed.", e.message]})

## `nautobot.apps.models.ContentTypeRelatedQuerySet`

Bases: `RestrictedQuerySet`

Source code in `nautobot/core/models/name_color_content_types.py`

    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    class ContentTypeRelatedQuerySet(RestrictedQuerySet):
        def get_for_model(self, model):
            """
            Return all `self.model` instances assigned to the given model.
            """
            content_type = ContentType.objects.get_for_model(model._meta.concrete_model)
            return self.filter(content_types=content_type)

        # TODO(timizuo): Merge into get_for_model; Cant do this now cause it would require alot
        #  of refactoring
        def get_for_models(self, models_):
            """
            Return all `self.model` instances assigned to the given `_models`.
            """
            q = Q()
            for model in models_:
                q |= Q(app_label=model._meta.app_label, model=model._meta.model_name)
            content_types = ContentType.objects.filter(q)
            return self.filter(content_types__in=content_types)

### `get_for_model(model)`

Return all `self.model` instances assigned to the given model.

Source code in `nautobot/core/models/name_color_content_types.py`

    19
    20
    21
    22
    23
    24
    def get_for_model(self, model):
        """
        Return all `self.model` instances assigned to the given model.
        """
        content_type = ContentType.objects.get_for_model(model._meta.concrete_model)
        return self.filter(content_types=content_type)

### `get_for_models(models_)`

Return all `self.model` instances assigned to the given `_models`.

Source code in `nautobot/core/models/name_color_content_types.py`

    28
    29
    30
    31
    32
    33
    34
    35
    36
    def get_for_models(self, models_):
        """
        Return all `self.model` instances assigned to the given `_models`.
        """
        q = Q()
        for model in models_:
            q |= Q(app_label=model._meta.app_label, model=model._meta.model_name)
        content_types = ContentType.objects.filter(q)
        return self.filter(content_types__in=content_types)

## `nautobot.apps.models.CustomFieldModel`

Bases: `models.Model`

Abstract class for any model which may have custom fields associated with it.

Source code in `nautobot/extras/models/customfields.py`

    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    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
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    class CustomFieldModel(models.Model):
        """
        Abstract class for any model which may have custom fields associated with it.
        """

        _custom_field_data = models.JSONField(encoder=DjangoJSONEncoder, blank=True, default=dict)

        class Meta:
            abstract = True

        @property
        def custom_field_data(self):
            """
            Legacy interface to raw custom field data

            TODO(John): remove this entirely when the cf property is enhanced
            """
            return self._custom_field_data

        @property
        def cf(self):
            """
            Convenience wrapper for custom field data.
            """
            return self._custom_field_data

        def get_custom_fields_basic(self):
            """
            This method exists to help call get_custom_fields() in templates where a function argument (advanced_ui) cannot be specified.
            Return a dictionary of custom fields for a single object in the form {<field>: value}
            which have advanced_ui set to False
            """
            return self.get_custom_fields(advanced_ui=False)

        def get_custom_fields_advanced(self):
            """
            This method exists to help call get_custom_fields() in templates where a function argument (advanced_ui) cannot be specified.
            Return a dictionary of custom fields for a single object in the form {<field>: value}
            which have advanced_ui set to True
            """
            return self.get_custom_fields(advanced_ui=True)

        def get_custom_fields(self, advanced_ui=None):
            """
            Return a dictionary of custom fields for a single object in the form {<field>: value}.
            """
            fields = CustomField.objects.get_for_model(self)
            if advanced_ui is not None:
                fields = fields.filter(advanced_ui=advanced_ui)
            return OrderedDict([(field, self.cf.get(field.key)) for field in fields])

        def get_custom_field_groupings_basic(self):
            """
            This method exists to help call get_custom_field_groupings() in templates where a function argument (advanced_ui) cannot be specified.
            Return a dictonary of custom fields grouped by the same grouping in the form
            {
                <grouping_1>: [(cf1, <value for cf1>), (cf2, <value for cf2>), ...],
                ...
                <grouping_5>: [(cf8, <value for cf8>), (cf9, <value for cf9>), ...],
                ...
            }
            which have advanced_ui set to False
            """
            return self.get_custom_field_groupings(advanced_ui=False)

        def get_custom_field_groupings_advanced(self):
            """
            This method exists to help call get_custom_field_groupings() in templates where a function argument (advanced_ui) cannot be specified.
            Return a dictonary of custom fields grouped by the same grouping in the form
            {
                <grouping_1>: [(cf1, <value for cf1>), (cf2, <value for cf2>), ...],
                ...
                <grouping_5>: [(cf8, <value for cf8>), (cf9, <value for cf9>), ...],
                ...
            }
            which have advanced_ui set to True
            """
            return self.get_custom_field_groupings(advanced_ui=True)

        def get_custom_field_groupings(self, advanced_ui=None):
            """
            Return a dictonary of custom fields grouped by the same grouping in the form
            {
                <grouping_1>: [(cf1, <value for cf1>), (cf2, <value for cf2>), ...],
                ...
                <grouping_5>: [(cf8, <value for cf8>), (cf9, <value for cf9>), ...],
                ...
            }
            """
            record = {}
            fields = CustomField.objects.get_for_model(self)
            if advanced_ui is not None:
                fields = fields.filter(advanced_ui=advanced_ui)

            for field in fields:
                data = (field, self.cf.get(field.key))
                record.setdefault(field.grouping, []).append(data)
            record = dict(sorted(record.items()))
            return record

        def clean(self):
            super().clean()

            custom_fields = {cf.key: cf for cf in CustomField.objects.get_for_model(self)}

            # Validate all field values
            for field_key, value in self._custom_field_data.items():
                if field_key not in custom_fields:
                    # log a warning instead of raising a ValidationError so as not to break the UI
                    logger.warning(f"Unknown field key '{field_key}' in custom field data for {self} ({self.pk}).")
                    continue
                try:
                    self._custom_field_data[field_key] = custom_fields[field_key].validate(value)
                except ValidationError as e:
                    raise ValidationError(f"Invalid value for custom field '{field_key}': {e.message}")

            # Check for missing values, erroring on required ones and populating non-required ones automatically
            for cf in custom_fields.values():
                if cf.key not in self._custom_field_data:
                    if cf.default is not None:
                        self._custom_field_data[cf.key] = cf.default
                    elif cf.required:
                        raise ValidationError(f"Missing required custom field '{cf.key}'.")

        # Computed Field Methods
        def has_computed_fields(self, advanced_ui=None):
            """
            Return a boolean indicating whether or not this content type has computed fields associated with it.
            This can also check whether the advanced_ui attribute is True or False for UI display purposes.
            """
            computed_fields = ComputedField.objects.get_for_model(self)
            if advanced_ui is not None:
                computed_fields = computed_fields.filter(advanced_ui=advanced_ui)
            return computed_fields.exists()

        def has_computed_fields_basic(self):
            return self.has_computed_fields(advanced_ui=False)

        def has_computed_fields_advanced(self):
            return self.has_computed_fields(advanced_ui=True)

        def get_computed_field(self, key, render=True):
            """
            Get a computed field for this model, lookup via key.
            Returns the template of this field if render is False, otherwise returns the rendered value.
            """
            try:
                computed_field = ComputedField.objects.get_for_model(self).get(key=key)
            except ComputedField.DoesNotExist:
                logger.warning("Computed Field with key %s does not exist for model %s", key, self._meta.verbose_name)
                return None
            if render:
                return computed_field.render(context={"obj": self})
            return computed_field.template

        def get_computed_fields(self, label_as_key=False, advanced_ui=None):
            """
            Return a dictionary of all computed fields and their rendered values for this model.
            Keys are the `key` value of each field. If label_as_key is True, `label` values of each field are used as keys.
            """
            computed_fields_dict = {}
            computed_fields = ComputedField.objects.get_for_model(self)
            if advanced_ui is not None:
                computed_fields = computed_fields.filter(advanced_ui=advanced_ui)
            if not computed_fields:
                return {}
            for cf in computed_fields:
                computed_fields_dict[cf.label if label_as_key else cf.key] = cf.render(context={"obj": self})
            return computed_fields_dict

### `cf` `property`

Convenience wrapper for custom field data.

### `custom_field_data` `property`

Legacy interface to raw custom field data

TODO(John): remove this entirely when the cf property is enhanced

### `get_computed_field(key, render=True)`

Get a computed field for this model, lookup via key. Returns the template of this field if render is False, otherwise returns the rendered value.

Source code in `nautobot/extras/models/customfields.py`

    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    def get_computed_field(self, key, render=True):
        """
        Get a computed field for this model, lookup via key.
        Returns the template of this field if render is False, otherwise returns the rendered value.
        """
        try:
            computed_field = ComputedField.objects.get_for_model(self).get(key=key)
        except ComputedField.DoesNotExist:
            logger.warning("Computed Field with key %s does not exist for model %s", key, self._meta.verbose_name)
            return None
        if render:
            return computed_field.render(context={"obj": self})
        return computed_field.template

### `get_computed_fields(label_as_key=False, advanced_ui=None)`

Return a dictionary of all computed fields and their rendered values for this model. Keys are the `key` value of each field. If label_as_key is True, `label` values of each field are used as keys.

Source code in `nautobot/extras/models/customfields.py`

    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    def get_computed_fields(self, label_as_key=False, advanced_ui=None):
        """
        Return a dictionary of all computed fields and their rendered values for this model.
        Keys are the `key` value of each field. If label_as_key is True, `label` values of each field are used as keys.
        """
        computed_fields_dict = {}
        computed_fields = ComputedField.objects.get_for_model(self)
        if advanced_ui is not None:
            computed_fields = computed_fields.filter(advanced_ui=advanced_ui)
        if not computed_fields:
            return {}
        for cf in computed_fields:
            computed_fields_dict[cf.label if label_as_key else cf.key] = cf.render(context={"obj": self})
        return computed_fields_dict

### `get_custom_field_groupings(advanced_ui=None)`

Return a dictonary of custom fields grouped by the same grouping in the form { : [(cf1, ), (cf2, ), ...], ... : [(cf8, ), (cf9, ), ...], ... }

Source code in `nautobot/extras/models/customfields.py`

    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    def get_custom_field_groupings(self, advanced_ui=None):
        """
        Return a dictonary of custom fields grouped by the same grouping in the form
        {
            <grouping_1>: [(cf1, <value for cf1>), (cf2, <value for cf2>), ...],
            ...
            <grouping_5>: [(cf8, <value for cf8>), (cf9, <value for cf9>), ...],
            ...
        }
        """
        record = {}
        fields = CustomField.objects.get_for_model(self)
        if advanced_ui is not None:
            fields = fields.filter(advanced_ui=advanced_ui)

        for field in fields:
            data = (field, self.cf.get(field.key))
            record.setdefault(field.grouping, []).append(data)
        record = dict(sorted(record.items()))
        return record

### `get_custom_field_groupings_advanced()`

This method exists to help call get_custom_field_groupings() in templates where a function argument (advanced_ui) cannot be specified. Return a dictonary of custom fields grouped by the same grouping in the form { : [(cf1, ), (cf2, ), ...], ... : [(cf8, ), (cf9, ), ...], ... } which have advanced_ui set to True

Source code in `nautobot/extras/models/customfields.py`

    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    def get_custom_field_groupings_advanced(self):
        """
        This method exists to help call get_custom_field_groupings() in templates where a function argument (advanced_ui) cannot be specified.
        Return a dictonary of custom fields grouped by the same grouping in the form
        {
            <grouping_1>: [(cf1, <value for cf1>), (cf2, <value for cf2>), ...],
            ...
            <grouping_5>: [(cf8, <value for cf8>), (cf9, <value for cf9>), ...],
            ...
        }
        which have advanced_ui set to True
        """
        return self.get_custom_field_groupings(advanced_ui=True)

### `get_custom_field_groupings_basic()`

This method exists to help call get_custom_field_groupings() in templates where a function argument (advanced_ui) cannot be specified. Return a dictonary of custom fields grouped by the same grouping in the form { : [(cf1, ), (cf2, ), ...], ... : [(cf8, ), (cf9, ), ...], ... } which have advanced_ui set to False

Source code in `nautobot/extras/models/customfields.py`

    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    def get_custom_field_groupings_basic(self):
        """
        This method exists to help call get_custom_field_groupings() in templates where a function argument (advanced_ui) cannot be specified.
        Return a dictonary of custom fields grouped by the same grouping in the form
        {
            <grouping_1>: [(cf1, <value for cf1>), (cf2, <value for cf2>), ...],
            ...
            <grouping_5>: [(cf8, <value for cf8>), (cf9, <value for cf9>), ...],
            ...
        }
        which have advanced_ui set to False
        """
        return self.get_custom_field_groupings(advanced_ui=False)

### `get_custom_fields(advanced_ui=None)`

Return a dictionary of custom fields for a single object in the form {: value}.

Source code in `nautobot/extras/models/customfields.py`

    177
    178
    179
    180
    181
    182
    183
    184
    def get_custom_fields(self, advanced_ui=None):
        """
        Return a dictionary of custom fields for a single object in the form {<field>: value}.
        """
        fields = CustomField.objects.get_for_model(self)
        if advanced_ui is not None:
            fields = fields.filter(advanced_ui=advanced_ui)
        return OrderedDict([(field, self.cf.get(field.key)) for field in fields])

### `get_custom_fields_advanced()`

This method exists to help call get_custom_fields() in templates where a function argument (advanced_ui) cannot be specified. Return a dictionary of custom fields for a single object in the form {: value} which have advanced_ui set to True

Source code in `nautobot/extras/models/customfields.py`

    169
    170
    171
    172
    173
    174
    175
    def get_custom_fields_advanced(self):
        """
        This method exists to help call get_custom_fields() in templates where a function argument (advanced_ui) cannot be specified.
        Return a dictionary of custom fields for a single object in the form {<field>: value}
        which have advanced_ui set to True
        """
        return self.get_custom_fields(advanced_ui=True)

### `get_custom_fields_basic()`

This method exists to help call get_custom_fields() in templates where a function argument (advanced_ui) cannot be specified. Return a dictionary of custom fields for a single object in the form {: value} which have advanced_ui set to False

Source code in `nautobot/extras/models/customfields.py`

    161
    162
    163
    164
    165
    166
    167
    def get_custom_fields_basic(self):
        """
        This method exists to help call get_custom_fields() in templates where a function argument (advanced_ui) cannot be specified.
        Return a dictionary of custom fields for a single object in the form {<field>: value}
        which have advanced_ui set to False
        """
        return self.get_custom_fields(advanced_ui=False)

### `has_computed_fields(advanced_ui=None)`

Return a boolean indicating whether or not this content type has computed fields associated with it. This can also check whether the advanced_ui attribute is True or False for UI display purposes.

Source code in `nautobot/extras/models/customfields.py`

    260
    261
    262
    263
    264
    265
    266
    267
    268
    def has_computed_fields(self, advanced_ui=None):
        """
        Return a boolean indicating whether or not this content type has computed fields associated with it.
        This can also check whether the advanced_ui attribute is True or False for UI display purposes.
        """
        computed_fields = ComputedField.objects.get_for_model(self)
        if advanced_ui is not None:
            computed_fields = computed_fields.filter(advanced_ui=advanced_ui)
        return computed_fields.exists()

## `nautobot.apps.models.CustomValidator`

This class is used to register plugin custom model validators which act on specified models. It contains the clean method which is overridden by plugin authors to execute custom validation logic. Plugin authors must raise ValidationError within this method to trigger validation error messages which are propagated to the user. A convenience method `validation_error(<message>)` may be used for this purpose.

The `model` attribute on the class defines the model to which this validator is registered. It should be set as a string in the form `<app_label>.<model_name>`.

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

    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    555
    556
    557
    558
    559
    560
    561
    562
    563
    564
    565
    class CustomValidator:
        """
        This class is used to register plugin custom model validators which act on specified models. It contains the clean
        method which is overridden by plugin authors to execute custom validation logic. Plugin authors must raise
        ValidationError within this method to trigger validation error messages which are propagated to the user.
        A convenience method `validation_error(<message>)` may be used for this purpose.

        The `model` attribute on the class defines the model to which this validator is registered. It
        should be set as a string in the form `<app_label>.<model_name>`.
        """

        model = None

        def __init__(self, obj):
            self.context = {"object": obj}

        def validation_error(self, message):
            """
            Convenience method for raising `django.core.exceptions.ValidationError` which is required in order to
            trigger validation error messages which are propagated to the user.
            """
            raise ValidationError(message)

        def clean(self):
            """
            Implement custom model validation in the standard Django clean method pattern. The model instance is accessed
            with the `object` key within `self.context`, e.g. `self.context['object']`. ValidationError must be raised to
            prevent saving model instance changes, and propagate messages to the user. For convenience,
            `self.validation_error(<message>)` may be called to raise a ValidationError.
            """
            raise NotImplementedError

### `clean()`

Implement custom model validation in the standard Django clean method pattern. The model instance is accessed with the `object` key within `self.context`, e.g. `self.context['object']`. ValidationError must be raised to prevent saving model instance changes, and propagate messages to the user. For convenience, `self.validation_error(<message>)` may be called to raise a ValidationError.

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

    558
    559
    560
    561
    562
    563
    564
    565
    def clean(self):
        """
        Implement custom model validation in the standard Django clean method pattern. The model instance is accessed
        with the `object` key within `self.context`, e.g. `self.context['object']`. ValidationError must be raised to
        prevent saving model instance changes, and propagate messages to the user. For convenience,
        `self.validation_error(<message>)` may be called to raise a ValidationError.
        """
        raise NotImplementedError

### `validation_error(message)`

Convenience method for raising `django.core.exceptions.ValidationError` which is required in order to trigger validation error messages which are propagated to the user.

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

    551
    552
    553
    554
    555
    556
    def validation_error(self, message):
        """
        Convenience method for raising `django.core.exceptions.ValidationError` which is required in order to
        trigger validation error messages which are propagated to the user.
        """
        raise ValidationError(message)

## `nautobot.apps.models.DynamicGroupMixin`

Adds properties to a model to facilitate reversing DynamicGroup membership:

- `dynamic_groups` - A QuerySet of `DynamicGroup` objects this instance is a member of, performs the most database queries.
- `dynamic_groups_cached` - A QuerySet of `DynamicGroup` objects this instance is a member of, uses cached member list if available. Ideal for most use cases.
- `dynamic_groups_list` - A list of `DynamicGroup` objects this instance is a member of, performs one less database query than `dynamic_groups`.
- `dynamic_groups_list_cached` - A list of `DynamicGroup` objects this instance is a member of, uses cached member list if available. Performs no database queries in optimal conditions.

All properties are cached on the instance after the first call. To clear the instance cache without re-instantiating the object, call `delattr(instance, "_[the_property_name]")`. EX: `delattr(instance, "_dynamic_groups")`

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

      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     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
    class DynamicGroupMixin:
        """
        Adds properties to a model to facilitate reversing DynamicGroup membership:

        - `dynamic_groups` - A QuerySet of `DynamicGroup` objects this instance is a member of, performs the most database queries.
        - `dynamic_groups_cached` - A QuerySet of `DynamicGroup` objects this instance is a member of, uses cached member list if available. Ideal for most use cases.
        - `dynamic_groups_list` - A list of `DynamicGroup` objects this instance is a member of, performs one less database query than `dynamic_groups`.
        - `dynamic_groups_list_cached` - A list of `DynamicGroup` objects this instance is a member of, uses cached member list if available. Performs no database queries in optimal conditions.

        All properties are cached on the instance after the first call. To clear the instance cache without re-instantiating the object, call `delattr(instance, "_[the_property_name]")`.
            EX: `delattr(instance, "_dynamic_groups")`
        """

        @property
        def dynamic_groups(self):
            """
            Return a queryset of `DynamicGroup` objects this instance is a member of.

            This will NOT use the cached member lists of the dynamic groups and will always query the database for each DynamicGroup.

            Additionally, this performs a final database query to turn the internal list into a queryset.
            """
            from nautobot.extras.models.groups import DynamicGroup

            if not hasattr(self, "_dynamic_groups"):
                queryset = DynamicGroup.objects.get_for_object(self)
                self._dynamic_groups = queryset

            return self._dynamic_groups

        @property
        def dynamic_groups_cached(self):
            """
            Return a queryset of `DynamicGroup` objects this instance is a member of.

            This will use the cached member lists of the dynamic groups if available.

            In optimal conditions this will incur a single database query to convert internal list into a queryset which is reasonably performant.

            This is the ideal property to use for most use cases.
            """
            from nautobot.extras.models.groups import DynamicGroup

            if not hasattr(self, "_dynamic_groups_cached"):
                queryset = DynamicGroup.objects.get_for_object(self, use_cache=True)
                self._dynamic_groups_cached = queryset

            return self._dynamic_groups_cached

        @property
        def dynamic_groups_list(self):
            """
            Return a list of `DynamicGroup` objects this instance is a member of.

            This will NOT use the cached member lists of the dynamic groups and will always query the database for each DynamicGroup.

            This saves a final query to turn the list into a queryset.
            """
            from nautobot.extras.models.groups import DynamicGroup

            if not hasattr(self, "_dynamic_groups_list"):
                dg_list = DynamicGroup.objects.get_list_for_object(self)
                self._dynamic_groups_list = dg_list

            return self._dynamic_groups_list

        @property
        def dynamic_groups_list_cached(self):
            """
            Return a list of `DynamicGroup` objects this instance is a member of.

            This will use the cached member lists of the dynamic groups if available.

            In optimal conditions this will incur no database queries.
            """

            from nautobot.extras.models.groups import DynamicGroup

            if not hasattr(self, "_dynamic_groups_list_cached"):
                dg_list = DynamicGroup.objects.get_list_for_object(self, use_cache=True)
                self._dynamic_groups_list_cached = dg_list

            return self._dynamic_groups_list_cached

        def get_dynamic_groups_url(self):
            """Return the dynamic groups URL for a given instance."""
            route = get_route_for_model(self, "dynamicgroups")

            # Iterate the pk-like fields and try to get a URL, or return None.
            fields = ["pk", "slug"]
            for field in fields:
                if not hasattr(self, field):
                    continue

                try:
                    return reverse(route, kwargs={field: getattr(self, field)})
                except NoReverseMatch:
                    continue

            return None

### `dynamic_groups` `property`

Return a queryset of `DynamicGroup` objects this instance is a member of.

This will NOT use the cached member lists of the dynamic groups and will always query the database for each DynamicGroup.

Additionally, this performs a final database query to turn the internal list into a queryset.

### `dynamic_groups_cached` `property`

Return a queryset of `DynamicGroup` objects this instance is a member of.

This will use the cached member lists of the dynamic groups if available.

In optimal conditions this will incur a single database query to convert internal list into a queryset which is reasonably performant.

This is the ideal property to use for most use cases.

### `dynamic_groups_list` `property`

Return a list of `DynamicGroup` objects this instance is a member of.

This will NOT use the cached member lists of the dynamic groups and will always query the database for each DynamicGroup.

This saves a final query to turn the list into a queryset.

### `dynamic_groups_list_cached` `property`

Return a list of `DynamicGroup` objects this instance is a member of.

This will use the cached member lists of the dynamic groups if available.

In optimal conditions this will incur no database queries.

### `get_dynamic_groups_url()`

Return the dynamic groups URL for a given instance.

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

     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    def get_dynamic_groups_url(self):
        """Return the dynamic groups URL for a given instance."""
        route = get_route_for_model(self, "dynamicgroups")

        # Iterate the pk-like fields and try to get a URL, or return None.
        fields = ["pk", "slug"]
        for field in fields:
            if not hasattr(self, field):
                continue

            try:
                return reverse(route, kwargs={field: getattr(self, field)})
            except NoReverseMatch:
                continue

        return None

## `nautobot.apps.models.EmptyGroupByJSONBAgg`

Bases: `JSONBAgg`

JSONBAgg is a builtin aggregation function which means it includes the use of a GROUP BY clause. When used as an annotation for collecting config context data objects, the GROUP BY is incorrect. This subclass overrides the Django ORM aggregation control to remove the GROUP BY.

Source code in `nautobot/core/models/query_functions.py`

    63
    64
    65
    66
    67
    68
    69
    70
    class EmptyGroupByJSONBAgg(JSONBAgg):
        """
        JSONBAgg is a builtin aggregation function which means it includes the use of a GROUP BY clause.
        When used as an annotation for collecting config context data objects, the GROUP BY is
        incorrect. This subclass overrides the Django ORM aggregation control to remove the GROUP BY.
        """

        contains_aggregate = False

## `nautobot.apps.models.EnhancedURLValidator`

Bases: `URLValidator`

Extends Django's built-in URLValidator to permit the use of hostnames with no domain extension and enforce allowed schemes specified in the configuration.

Source code in `nautobot/core/models/validators.py`

     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class EnhancedURLValidator(URLValidator):
        """
        Extends Django's built-in URLValidator to permit the use of hostnames with no domain extension and enforce allowed
        schemes specified in the configuration.
        """

        fqdn_re = URLValidator.hostname_re + URLValidator.domain_re + URLValidator.tld_re
        host_res = [
            URLValidator.ipv4_re,
            URLValidator.ipv6_re,
            fqdn_re,
            URLValidator.hostname_re,
        ]
        regex = _lazy_re_compile(
            r"^(?:[a-z0-9\.\-\+]*)://"  # Scheme (enforced separately)
            r"(?:\S+(?::\S*)?@)?"  # HTTP basic authentication
            r"(?:" + "|".join(host_res) + ")"  # IPv4, IPv6, FQDN, or hostname
            r"(?::\d{2,5})?"  # Port number
            r"(?:[/?#][^\s]*)?"  # Path
            r"\Z",
            re.IGNORECASE,
        )
        schemes = settings.ALLOWED_URL_SCHEMES

## `nautobot.apps.models.ExclusionValidator`

Bases: `BaseValidator`

Ensure that a field's value is not equal to any of the specified values.

Source code in `nautobot/core/models/validators.py`

    33
    34
    35
    36
    37
    38
    39
    40
    41
    class ExclusionValidator(BaseValidator):
        """
        Ensure that a field's value is not equal to any of the specified values.
        """

        message = "This value may not be %(show_value)s."

        def compare(self, a, b):
            return a in b

## `nautobot.apps.models.ForeignKeyLimitedByContentTypes`

Bases: `ForeignKeyWithAutoRelatedName`

An abstract model field that automatically restricts ForeignKey options based on content_types.

For instance, if the model "Role" contains two records: role_1 and role_2, role_1's content_types are set to "dcim.location" and "dcim.device" while the role_2's content_types are set to "circuit.circuit" and "dcim.location."

Then, for the field `role` on the Device model, role_1 is the only Role that is available, while role_1 & role_2 are both available for the Location model.

The limit_choices_to for the field are automatically derived from

- the content-type to which the field is attached (e.g. `dcim.device`)

Source code in `nautobot/core/models/fields.py`

    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    class ForeignKeyLimitedByContentTypes(ForeignKeyWithAutoRelatedName):
        """
        An abstract model field that automatically restricts ForeignKey options based on content_types.

        For instance, if the model "Role" contains two records: role_1 and role_2, role_1's content_types
        are set to "dcim.location" and "dcim.device" while the role_2's content_types are set to
        "circuit.circuit" and "dcim.location."

        Then, for the field `role` on the Device model, role_1 is the only Role that is available,
        while role_1 & role_2 are both available for the Location model.

        The limit_choices_to for the field are automatically derived from:
            - the content-type to which the field is attached (e.g. `dcim.device`)
        """

        def get_limit_choices_to(self):
            """
            Limit this field to only objects which are assigned to this model's content-type.

            Note that this is implemented via specifying `content_types__app_label=` and `content_types__model=`
            rather than via the more obvious `content_types=ContentType.objects.get_for_model(self.model)`
            because the latter approach would involve a database query, and in some cases
            (most notably FilterSet definition) this function is called **before** database migrations can be run.
            """
            return {
                "content_types__app_label": self.model._meta.app_label,
                "content_types__model": self.model._meta.model_name,
            }

        def formfield(self, **kwargs):
            """Return a prepped formfield for use in model forms."""
            defaults = {
                "form_class": fields.DynamicModelChoiceField,
                "queryset": self.related_model.objects.all(),
                # label_lower e.g. "dcim.device"
                "query_params": {"content_types": self.model._meta.label_lower},
            }
            defaults.update(**kwargs)
            return super().formfield(**defaults)

### `formfield(**kwargs)`

Return a prepped formfield for use in model forms.

Source code in `nautobot/core/models/fields.py`

    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    def formfield(self, **kwargs):
        """Return a prepped formfield for use in model forms."""
        defaults = {
            "form_class": fields.DynamicModelChoiceField,
            "queryset": self.related_model.objects.all(),
            # label_lower e.g. "dcim.device"
            "query_params": {"content_types": self.model._meta.label_lower},
        }
        defaults.update(**kwargs)
        return super().formfield(**defaults)

### `get_limit_choices_to()`

Limit this field to only objects which are assigned to this model's content-type.

Note that this is implemented via specifying `content_types__app_label=` and `content_types__model=` rather than via the more obvious `content_types=ContentType.objects.get_for_model(self.model)` because the latter approach would involve a database query, and in some cases (most notably FilterSet definition) this function is called before database migrations can be run.

Source code in `nautobot/core/models/fields.py`

    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    def get_limit_choices_to(self):
        """
        Limit this field to only objects which are assigned to this model's content-type.

        Note that this is implemented via specifying `content_types__app_label=` and `content_types__model=`
        rather than via the more obvious `content_types=ContentType.objects.get_for_model(self.model)`
        because the latter approach would involve a database query, and in some cases
        (most notably FilterSet definition) this function is called **before** database migrations can be run.
        """
        return {
            "content_types__app_label": self.model._meta.app_label,
            "content_types__model": self.model._meta.model_name,
        }

## `nautobot.apps.models.ForeignKeyWithAutoRelatedName`

Bases: `models.ForeignKey`

Extend base ForeignKey functionality to create a smarter default `related_name`.

For example, "ip_addresses" instead of "ipaddress_set", "ipaddresss", or "ipam_ipaddress_related".

Primarily useful for cases of abstract base classes that define ForeignKeys, such as `nautobot.dcim.models.device_components.ComponentModel`.

Source code in `nautobot/core/models/fields.py`

    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    class ForeignKeyWithAutoRelatedName(models.ForeignKey):
        """
        Extend base ForeignKey functionality to create a smarter default `related_name`.

        For example, "ip_addresses" instead of "ipaddress_set", "ipaddresss", or "ipam_ipaddress_related".

        Primarily useful for cases of abstract base classes that define ForeignKeys, such as
        `nautobot.dcim.models.device_components.ComponentModel`.
        """

        def __init__(self, *args, related_name=None, **kwargs):
            super().__init__(*args, related_name=related_name, **kwargs)
            self._autogenerate_related_name = related_name is None

        def contribute_to_class(self, cls, *args, **kwargs):
            super().contribute_to_class(cls, *args, **kwargs)

            if self._autogenerate_related_name and not cls._meta.abstract and hasattr(cls._meta, "verbose_name_plural"):
                # "IP addresses" -> "ip_addresses"
                related_name = "_".join(re.findall(r"\w+", str(cls._meta.verbose_name_plural))).lower()
                self.remote_field.related_name = related_name

## `nautobot.apps.models.JSONArrayField`

Bases: `models.JSONField`

An ArrayField implementation backed JSON storage. Replicates ArrayField's base field validation.

Source code in `nautobot/core/models/fields.py`

    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    class JSONArrayField(models.JSONField):
        """
        An ArrayField implementation backed JSON storage.
        Replicates ArrayField's base field validation.
        """

        _default_hint = ("list", "[]")

        def __init__(self, base_field, **kwargs):
            if isinstance(base_field, JSONArrayField):
                raise TypeError("cannot nest JSONArrayFields")
            self.base_field = base_field
            super().__init__(**kwargs)

        def set_attributes_from_name(self, name):
            super().set_attributes_from_name(name)
            self.base_field.set_attributes_from_name(name)

        @property
        def description(self):
            return f"JSON Array of {self.base_field.description}"

        def get_prep_value(self, value):
            """Perform preliminary non-db specific value checks and conversions."""
            if value is not None:
                if not isinstance(value, (list, tuple)):
                    raise ValueError(f"value {value} is not list or tuple")
                value = [self.base_field.get_prep_value(v) for v in value]
            return super().get_prep_value(value)

        def deconstruct(self):
            """
            Return enough information to recreate the field as a 4-tuple:
             * The name of the field on the model, if contribute_to_class() has
               been run.
             * The import path of the field, including the class:e.g.
               django.db.models.IntegerField This should be the most portable
               version, so less specific may be better.
             * A list of positional arguments.
             * A dict of keyword arguments.
            """
            name, path, args, kwargs = super().deconstruct()
            kwargs.update(
                {
                    "base_field": self.base_field.clone(),
                }
            )
            return name, path, args, kwargs

        def to_python(self, value):
            """
            Convert `value` into JSON, raising django.core.exceptions.ValidationError
            if the data can't be converted. Return the converted value.
            """
            if isinstance(value, str):
                try:
                    # Assume we're deserializing
                    vals = json.loads(value)
                    value = [self.base_field.to_python(val) for val in vals]
                except (TypeError, json.JSONDecodeError) as e:
                    raise exceptions.ValidationError(e)
            return value

        def value_to_string(self, obj):
            """
            Return a string value of this field from the passed obj.
            This is used by the serialization framework.
            """
            values = []
            vals = self.value_from_object(obj)
            base_field = self.base_field

            for val in vals:
                if val is None:
                    values.append(None)
                else:
                    obj = AttributeSetter(base_field.attname, val)
                    values.append(base_field.value_to_string(obj))
            return json.dumps(values, ensure_ascii=False)

        def validate(self, value, model_instance):
            """
            Validate `value` and raise ValidationError if necessary.
            """
            super().validate(value, model_instance)
            for part in value:
                self.base_field.validate(part, model_instance)

        def run_validators(self, value):
            """
            Runs all validators against `value` and raise ValidationError if necessary.
            Some validators can't be created at field initialization time.
            """
            super().run_validators(value)
            for part in value:
                self.base_field.run_validators(part)

        def formfield(self, **kwargs):
            """Return a django.forms.Field instance for this field."""
            return super().formfield(
                **{
                    "form_class": fields.JSONArrayFormField,
                    "base_field": self.base_field.formfield(),
                    **kwargs,
                }
            )

### `deconstruct()`

Return enough information to recreate the field as a 4-tuple

- The name of the field on the model, if contribute_to_class() has been run.
- The import path of the field, including the class:e.g. django.db.models.IntegerField This should be the most portable version, so less specific may be better.
- A list of positional arguments.
- A dict of keyword arguments.

Source code in `nautobot/core/models/fields.py`

    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    def deconstruct(self):
        """
        Return enough information to recreate the field as a 4-tuple:
         * The name of the field on the model, if contribute_to_class() has
           been run.
         * The import path of the field, including the class:e.g.
           django.db.models.IntegerField This should be the most portable
           version, so less specific may be better.
         * A list of positional arguments.
         * A dict of keyword arguments.
        """
        name, path, args, kwargs = super().deconstruct()
        kwargs.update(
            {
                "base_field": self.base_field.clone(),
            }
        )
        return name, path, args, kwargs

### `formfield(**kwargs)`

Return a django.forms.Field instance for this field.

Source code in `nautobot/core/models/fields.py`

    372
    373
    374
    375
    376
    377
    378
    379
    380
    def formfield(self, **kwargs):
        """Return a django.forms.Field instance for this field."""
        return super().formfield(
            **{
                "form_class": fields.JSONArrayFormField,
                "base_field": self.base_field.formfield(),
                **kwargs,
            }
        )

### `get_prep_value(value)`

Perform preliminary non-db specific value checks and conversions.

Source code in `nautobot/core/models/fields.py`

    297
    298
    299
    300
    301
    302
    303
    def get_prep_value(self, value):
        """Perform preliminary non-db specific value checks and conversions."""
        if value is not None:
            if not isinstance(value, (list, tuple)):
                raise ValueError(f"value {value} is not list or tuple")
            value = [self.base_field.get_prep_value(v) for v in value]
        return super().get_prep_value(value)

### `run_validators(value)`

Runs all validators against `value` and raise ValidationError if necessary. Some validators can't be created at field initialization time.

Source code in `nautobot/core/models/fields.py`

    363
    364
    365
    366
    367
    368
    369
    370
    def run_validators(self, value):
        """
        Runs all validators against `value` and raise ValidationError if necessary.
        Some validators can't be created at field initialization time.
        """
        super().run_validators(value)
        for part in value:
            self.base_field.run_validators(part)

### `to_python(value)`

Convert `value` into JSON, raising django.core.exceptions.ValidationError if the data can't be converted. Return the converted value.

Source code in `nautobot/core/models/fields.py`

    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    def to_python(self, value):
        """
        Convert `value` into JSON, raising django.core.exceptions.ValidationError
        if the data can't be converted. Return the converted value.
        """
        if isinstance(value, str):
            try:
                # Assume we're deserializing
                vals = json.loads(value)
                value = [self.base_field.to_python(val) for val in vals]
            except (TypeError, json.JSONDecodeError) as e:
                raise exceptions.ValidationError(e)
        return value

### `validate(value, model_instance)`

Validate `value` and raise ValidationError if necessary.

Source code in `nautobot/core/models/fields.py`

    355
    356
    357
    358
    359
    360
    361
    def validate(self, value, model_instance):
        """
        Validate `value` and raise ValidationError if necessary.
        """
        super().validate(value, model_instance)
        for part in value:
            self.base_field.validate(part, model_instance)

### `value_to_string(obj)`

Return a string value of this field from the passed obj. This is used by the serialization framework.

Source code in `nautobot/core/models/fields.py`

    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    def value_to_string(self, obj):
        """
        Return a string value of this field from the passed obj.
        This is used by the serialization framework.
        """
        values = []
        vals = self.value_from_object(obj)
        base_field = self.base_field

        for val in vals:
            if val is None:
                values.append(None)
            else:
                obj = AttributeSetter(base_field.attname, val)
                values.append(base_field.value_to_string(obj))
        return json.dumps(values, ensure_ascii=False)

## `nautobot.apps.models.JSONBAgg`

Bases: `Aggregate`

Like django.contrib.postgres.aggregates.JSONBAgg, but different.

1. Supports both Postgres (JSONB_AGG) and MySQL (JSON_ARRAYAGG)
2. Does not support `ordering` as JSON_ARRAYAGG does not guarantee ordering.

Source code in `nautobot/core/models/query_functions.py`

    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    class JSONBAgg(Aggregate):
        """
        Like django.contrib.postgres.aggregates.JSONBAgg, but different.

        1. Supports both Postgres (JSONB_AGG) and MySQL (JSON_ARRAYAGG)
        2. Does not support `ordering` as JSON_ARRAYAGG does not guarantee ordering.
        """

        function = None
        output_field = JSONField()
        # TODO(Glenn): Django's JSONBAgg has `allow_distinct=True`, we might want to think about adding that at some point?

        # Borrowed from `django.contrib.postgres.aggregates.JSONBagg`.
        def convert_value(self, value, expression, connection):  # pylint: disable=arguments-differ
            if not value:
                return "[]"
            return value

        def as_sql(self, compiler, connection, **extra_context):
            vendor = connection.vendor
            # Mapping of vendor => func
            func_map = {
                "postgresql": "JSONB_AGG",
                "mysql": "JSON_ARRAYAGG",
            }

            if JSONBAgg.function is None and vendor not in func_map:
                raise ConnectionError(f"JSON aggregation is not supported for database {vendor}")

            JSONBAgg.function = func_map[vendor]

            return super().as_sql(compiler, connection, **extra_context)

## `nautobot.apps.models.NameColorContentTypesModel`

Bases: `BaseModel`, `ChangeLoggedModel`, `CustomFieldModel`, `RelationshipModel`, `NotesMixin`, `DynamicGroupMixin`

This abstract base properties model contains fields and functionality that are shared amongst models that requires these fields: name, color, content_types and description.

Source code in `nautobot/core/models/name_color_content_types.py`

    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
    class NameColorContentTypesModel(
        BaseModel,
        ChangeLoggedModel,
        CustomFieldModel,
        RelationshipModel,
        NotesMixin,
        DynamicGroupMixin,
    ):
        """
        This abstract base properties model contains fields and functionality that are
        shared amongst models that requires these fields: name, color, content_types and description.
        """

        content_types = models.ManyToManyField(
            to=ContentType,
            help_text="The content type(s) to which this model applies.",
        )
        name = models.CharField(max_length=100, unique=True)
        color = ColorField(default=ColorChoices.COLOR_GREY)
        description = models.CharField(
            max_length=200,
            blank=True,
        )

        objects = BaseManager.from_queryset(ContentTypeRelatedQuerySet)()

        clone_fields = ["color", "content_types"]

        class Meta:
            ordering = ["name"]
            abstract = True

        def __str__(self):
            return self.name

        def get_content_types(self):
            return ",".join(f"{ct.app_label}.{ct.model}" for ct in self.content_types.all())

## `nautobot.apps.models.NaturalOrderingField`

Bases: `models.CharField`

A field which stores a naturalized representation of its target field, to be used for ordering its parent model.

:param target_field: Name of the field of the parent model to be naturalized :param naturalize_function: The function used to generate a naturalized value (optional)

Source code in `nautobot/core/models/fields.py`

    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    class NaturalOrderingField(models.CharField):
        """
        A field which stores a naturalized representation of its target field, to be used for ordering its parent model.

        :param target_field: Name of the field of the parent model to be naturalized
        :param naturalize_function: The function used to generate a naturalized value (optional)
        """

        description = "Stores a representation of its target field suitable for natural ordering"

        def __init__(self, target_field, naturalize_function=ordering.naturalize, *args, **kwargs):
            self.target_field = target_field
            self.naturalize_function = naturalize_function
            super().__init__(*args, **kwargs)

        def pre_save(self, model_instance, add):
            """
            Generate a naturalized value from the target field
            """
            original_value = getattr(model_instance, self.target_field)
            naturalized_value = self.naturalize_function(original_value, max_length=self.max_length)
            setattr(model_instance, self.attname, naturalized_value)

            return naturalized_value

        def deconstruct(self):
            kwargs = super().deconstruct()[3]  # Pass kwargs from CharField
            kwargs["naturalize_function"] = self.naturalize_function
            return (
                self.name,
                "nautobot.core.models.fields.NaturalOrderingField",
                [self.target_field],
                kwargs,
            )

### `pre_save(model_instance, add)`

Generate a naturalized value from the target field

Source code in `nautobot/core/models/fields.py`

    254
    255
    256
    257
    258
    259
    260
    261
    262
    def pre_save(self, model_instance, add):
        """
        Generate a naturalized value from the target field
        """
        original_value = getattr(model_instance, self.target_field)
        naturalized_value = self.naturalize_function(original_value, max_length=self.max_length)
        setattr(model_instance, self.attname, naturalized_value)

        return naturalized_value

## `nautobot.apps.models.NotesMixin`

Adds a `notes` property that returns a queryset of `Notes` membership.

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

    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    class NotesMixin:
        """
        Adds a `notes` property that returns a queryset of `Notes` membership.
        """

        @property
        def notes(self):
            """Return a `Notes` queryset for this instance."""
            from nautobot.extras.models.models import Note

            if not hasattr(self, "_notes_queryset"):
                queryset = Note.objects.get_for_object(self)
                self._notes_queryset = queryset

            return self._notes_queryset

        def get_notes_url(self, api=False):
            """Return the notes URL for a given instance."""
            route = get_route_for_model(self, "notes", api=api)

            # Iterate the pk-like fields and try to get a URL, or return None.
            fields = ["pk", "slug"]
            for field in fields:
                if not hasattr(self, field):
                    continue

                try:
                    return reverse(route, kwargs={field: getattr(self, field)})
                except NoReverseMatch:
                    continue

            return None

### `notes` `property`

Return a `Notes` queryset for this instance.

### `get_notes_url(api=False)`

Return the notes URL for a given instance.

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

    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    def get_notes_url(self, api=False):
        """Return the notes URL for a given instance."""
        route = get_route_for_model(self, "notes", api=api)

        # Iterate the pk-like fields and try to get a URL, or return None.
        fields = ["pk", "slug"]
        for field in fields:
            if not hasattr(self, field):
                continue

            try:
                return reverse(route, kwargs={field: getattr(self, field)})
            except NoReverseMatch:
                continue

        return None

## `nautobot.apps.models.OrganizationalModel`

Bases: `BaseModel`, `ChangeLoggedModel`, `CustomFieldModel`, `RelationshipModel`, `DynamicGroupMixin`, `NotesMixin`

Base abstract model for all organizational models.

Organizational models aid the primary models by building structured relationships and logical groups, or categorizations. Organizational models do not typically represent concrete networking resources or assets, but rather they enable user specific use cases and metadata about network resources. Examples include Device Role, Rack Group, Status, Manufacturer, and Platform.

Source code in `nautobot/core/models/generics.py`

    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class OrganizationalModel(
        BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel, DynamicGroupMixin, NotesMixin
    ):
        """
        Base abstract model for all organizational models.

        Organizational models aid the primary models by building structured relationships
        and logical groups, or categorizations. Organizational models do not typically
        represent concrete networking resources or assets, but rather they enable user
        specific use cases and metadata about network resources. Examples include
        Device Role, Rack Group, Status, Manufacturer, and Platform.
        """

        class Meta:
            abstract = True

## `nautobot.apps.models.PrimaryModel`

Bases: `BaseModel`, `ChangeLoggedModel`, `CustomFieldModel`, `RelationshipModel`, `DynamicGroupMixin`, `NotesMixin`

Base abstract model for all primary models.

A primary model is one which is materialistically relevant to the network datamodel. Such models form the basis of major elements of the data model, like Device, IP Address, Location, VLAN, Virtual Machine, etc. Primary models usually represent tangible or logical resources on the network, or within the organization.

Source code in `nautobot/core/models/generics.py`

    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    class PrimaryModel(BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel, DynamicGroupMixin, NotesMixin):
        """
        Base abstract model for all primary models.

        A primary model is one which is materialistically relevant to the network datamodel.
        Such models form the basis of major elements of the data model, like Device,
        IP Address, Location, VLAN, Virtual Machine, etc. Primary models usually represent
        tangible or logical resources on the network, or within the organization.
        """

        tags = TagsField()

        class Meta:
            abstract = True

## `nautobot.apps.models.RelationshipModel`

Bases: `models.Model`

Abstract class for any model which may have custom relationships associated with it.

Source code in `nautobot/extras/models/relationships.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
    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
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    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
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    class RelationshipModel(models.Model):
        """
        Abstract class for any model which may have custom relationships associated with it.
        """

        class Meta:
            abstract = True

        # Define GenericRelations so that deleting a RelationshipModel instance
        # cascades to deleting any RelationshipAssociations that were using this instance,
        # and also for convenience in looking up the RelationshipModels associated to any given RelationshipAssociation
        source_for_associations = GenericRelation(
            "extras.RelationshipAssociation",
            content_type_field="source_type",
            object_id_field="source_id",
            related_query_name="source_%(app_label)s_%(class)s",  # e.g. 'source_dcim_location', 'source_ipam_vlan'
        )
        destination_for_associations = GenericRelation(
            "extras.RelationshipAssociation",
            content_type_field="destination_type",
            object_id_field="destination_id",
            related_query_name="destination_%(app_label)s_%(class)s",  # e.g. 'destination_dcim_rack'
        )

        @property
        def associations(self):
            return list(self.source_for_associations.all()) + list(self.destination_for_associations.all())

        def get_relationships(self, include_hidden=False, advanced_ui=None):
            """
            Return a dictionary of RelationshipAssociation querysets for all custom relationships

            Returns:
                (dict): `{
                        "source": {
                            <Relationship instance #1>: <RelationshipAssociation queryset #1>,
                            <Relationship instance #2>: <RelationshipAssociation queryset #2>,
                        },
                        "destination": {
                            <Relationship instance #3>: <RelationshipAssociation queryset #3>,
                            <Relationship instance #4>: <RelationshipAssociation queryset #4>,
                        },
                        "peer": {
                            <Relationship instance #5>: <RelationshipAssociation queryset #5>,
                            <Relationship instance #6>: <RelationshipAssociation queryset #6>,
                        },
                    }`
            """
            src_relationships, dst_relationships = Relationship.objects.get_for_model(self)
            if advanced_ui is not None:
                src_relationships = src_relationships.filter(advanced_ui=advanced_ui)
                dst_relationships = dst_relationships.filter(advanced_ui=advanced_ui)
            content_type = ContentType.objects.get_for_model(self)

            sides = {
                RelationshipSideChoices.SIDE_SOURCE: src_relationships,
                RelationshipSideChoices.SIDE_DESTINATION: dst_relationships,
            }

            resp = {
                RelationshipSideChoices.SIDE_SOURCE: {},
                RelationshipSideChoices.SIDE_DESTINATION: {},
                RelationshipSideChoices.SIDE_PEER: {},
            }
            for side, relationships in sides.items():
                for relationship in relationships:
                    if getattr(relationship, f"{side}_hidden") and not include_hidden:
                        continue

                    # Determine if the relationship is applicable to this object based on the filter
                    # To resolve the filter we are using the FilterSet for the given model
                    # If there is no match when we query the primary key of the device along with the filter
                    # Then the relationship is not applicable to this object
                    if getattr(relationship, f"{side}_filter"):
                        filterset = get_filterset_for_model(self._meta.model)
                        if filterset:
                            filter_params = getattr(relationship, f"{side}_filter")
                            if not filterset(filter_params, self._meta.model.objects.filter(id=self.id)).qs.exists():
                                continue

                    # Construct the queryset to query all RelationshipAssociation for this object and this relationship
                    query_params = {"relationship": relationship}
                    if not relationship.symmetric:
                        # Query for RelationshipAssociations that this object is on the expected side of
                        query_params[f"{side}_id"] = self.pk
                        query_params[f"{side}_type"] = content_type

                        resp[side][relationship] = RelationshipAssociation.objects.filter(**query_params)
                    else:
                        # Query for RelationshipAssociations involving this object, regardless of side
                        resp[RelationshipSideChoices.SIDE_PEER][relationship] = RelationshipAssociation.objects.filter(
                            (
                                Q(source_id=self.pk, source_type=content_type)
                                | Q(destination_id=self.pk, destination_type=content_type)
                            ),
                            **query_params,
                        )

            return resp

        def get_relationships_data(self, **kwargs):
            """
            Return a dictionary of relationships with the label and the value or the queryset for each.

            Used for rendering relationships in the UI; see nautobot/core/templates/inc/relationships_table_rows.html

            Returns:
                (dict): `{
                        "source": {
                            <Relationship instance #1>: {   # one-to-one relationship that self is the source of
                                "label": "...",
                                "peer_type": <ContentType>,
                                "has_many": False,
                                "value": <model instance>,     # single destination for this relationship
                                "url": "...",
                            },
                            <Relationship instance #2>: {   # one-to-many or many-to-many relationship that self is a source for
                                "label": "...",
                                "peer_type": <ContentType>,
                                "has_many": True,
                                "value": None,
                                "queryset": <RelationshipAssociation queryset #2>   # set of destinations for the relationship
                            },
                        },
                        "destination": {
                            (same format as "source" dict - relationships that self is the destination of)
                        },
                        "peer": {
                            (same format as "source" dict - symmetric relationships that self is involved in)
                        },
                    }`
            """

            relationships_by_side = self.get_relationships(**kwargs)

            resp = {
                RelationshipSideChoices.SIDE_SOURCE: {},
                RelationshipSideChoices.SIDE_DESTINATION: {},
                RelationshipSideChoices.SIDE_PEER: {},
            }
            for side, relationships in relationships_by_side.items():
                for relationship, queryset in relationships.items():
                    peer_side = RelationshipSideChoices.OPPOSITE[side]

                    resp[side][relationship] = {
                        "label": relationship.get_label(side),
                        "value": None,
                    }
                    if not relationship.symmetric:
                        resp[side][relationship]["peer_type"] = getattr(relationship, f"{peer_side}_type")
                    else:
                        # Symmetric relationship - source_type == destination_type, so it doesn't matter which we choose
                        resp[side][relationship]["peer_type"] = getattr(relationship, "source_type")

                    resp[side][relationship]["has_many"] = relationship.has_many(peer_side)

                    if resp[side][relationship]["has_many"]:
                        resp[side][relationship]["queryset"] = queryset
                    else:
                        resp[side][relationship]["url"] = None
                        association = queryset.first()
                        if not association:
                            continue

                        peer = association.get_peer(self)

                        resp[side][relationship]["value"] = peer
                        if hasattr(peer, "get_absolute_url"):
                            resp[side][relationship]["url"] = peer.get_absolute_url()
                        else:
                            logger.warning("Peer object %s has no get_absolute_url() method", peer)

            return resp

        def get_relationships_data_basic_fields(self):
            """
            Same docstring as get_relationships_data() above except this only returns relationships
            where advanced_ui==False for displaying in the main object detail tab on the object's page
            """
            return self.get_relationships_data(advanced_ui=False)

        def get_relationships_data_advanced_fields(self):
            """
            Same docstring as get_relationships_data() above except this only returns relationships
            where advanced_ui==True for displaying in the 'Advanced' tab on the object's page
            """
            return self.get_relationships_data(advanced_ui=True)

        @classmethod
        def required_related_objects_errors(
            cls, output_for="ui", initial_data=None, relationships_key_specified=False, instance=None
        ):
            """
            Args:
                output_for (str): either "ui" or "api" depending on usage
                initial_data (dict): submitted form/serializer data to validate against
                relationships_key_specified (bool): if the "relationships" key was provided or not
                instance (Optional[BaseModel]): an optional model instance to validate against
            Returns:
                (list[dict]): List of field error dicts if any are found
            """

            required_relationships = Relationship.objects.get_required_for_model(cls)
            relationships_field_errors = {}
            for relation in required_relationships:
                opposite_side = RelationshipSideChoices.OPPOSITE[relation.required_on]

                if relation.skip_required(cls, opposite_side):
                    continue

                if relation.has_many(opposite_side):
                    num_required_verbose = "at least one"
                else:
                    num_required_verbose = "a"

                if output_for == "api":
                    # If this is a model instance and the relationships json data key is missing, check to see if
                    # required relationship associations already exist, and continue (ignore validation) if so
                    if (
                        getattr(instance, "present_in_database", False) is True
                        and initial_data.get(relation, {}).get(opposite_side, {}) == {}
                        and not relationships_key_specified
                    ):
                        filter_kwargs = {"relationship": relation, f"{relation.required_on}_id": instance.pk}
                        if RelationshipAssociation.objects.filter(**filter_kwargs).exists():
                            continue

                required_model_class = getattr(relation, f"{opposite_side}_type").model_class()
                required_model_meta = required_model_class._meta
                cr_field_name = f"cr_{relation.key}__{opposite_side}"
                name_plural = cls._meta.verbose_name_plural
                field_key = relation.key if output_for == "api" else cr_field_name
                field_errors = {field_key: []}

                if not required_model_class.objects.exists():
                    hint = (
                        f"You need to create {num_required_verbose} {required_model_meta.verbose_name} "
                        f"before instantiating a {cls._meta.verbose_name}."
                    )

                    if output_for == "ui":
                        try:
                            add_url = reverse(get_route_for_model(required_model_class, "add"))
                            hint = format_html(
                                '<a target="_blank" href="{}">Click here</a> to create a {}.',
                                add_url,
                                required_model_meta.verbose_name,
                            )
                        except NoReverseMatch:
                            pass

                    elif output_for == "api":
                        try:
                            api_post_url = reverse(get_route_for_model(required_model_class, "list", api=True))
                            hint = f"Create a {required_model_meta.verbose_name} by posting to {api_post_url}"
                        except NoReverseMatch:
                            pass

                    error_message = format_html(
                        "{} require {} {}, but no {} exist yet. ",
                        bettertitle(name_plural),
                        num_required_verbose,
                        required_model_meta.verbose_name,
                        required_model_meta.verbose_name_plural,
                    )
                    error_message += hint
                    field_errors[field_key].append(error_message)

                if initial_data is not None:
                    supplied_data = []

                    if output_for == "ui":
                        supplied_data = initial_data.get(field_key, [])

                    elif output_for == "api":
                        supplied_data = initial_data.get(relation, {}).get(opposite_side, {})

                    if not supplied_data:
                        if output_for == "ui":
                            field_errors[field_key].append(
                                f"You need to select {num_required_verbose} {required_model_meta.verbose_name}."
                            )
                        elif output_for == "api":
                            field_errors[field_key].append(
                                f'You need to specify ["relationships"]["{relation.key}"]["{opposite_side}"]["objects"].'
                            )

                if len(field_errors[field_key]) > 0:
                    relationships_field_errors[field_key] = field_errors[field_key]

            return relationships_field_errors

### `get_relationships(include_hidden=False, advanced_ui=None)`

Return a dictionary of RelationshipAssociation querysets for all custom relationships

Returns:

Type | Description
--- | ---
`dict` | `{ "source": { <Relationship instance #1>: <RelationshipAssociation queryset #1>, <Relationship instance #2>: <RelationshipAssociation queryset #2>, }, "destination": { <Relationship instance #3>: <RelationshipAssociation queryset #3>, <Relationship instance #4>: <RelationshipAssociation queryset #4>, }, "peer": { <Relationship instance #5>: <RelationshipAssociation queryset #5>, <Relationship instance #6>: <RelationshipAssociation queryset #6>, }, }`

Source code in `nautobot/extras/models/relationships.py`

     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    def get_relationships(self, include_hidden=False, advanced_ui=None):
        """
        Return a dictionary of RelationshipAssociation querysets for all custom relationships

        Returns:
            (dict): `{
                    "source": {
                        <Relationship instance #1>: <RelationshipAssociation queryset #1>,
                        <Relationship instance #2>: <RelationshipAssociation queryset #2>,
                    },
                    "destination": {
                        <Relationship instance #3>: <RelationshipAssociation queryset #3>,
                        <Relationship instance #4>: <RelationshipAssociation queryset #4>,
                    },
                    "peer": {
                        <Relationship instance #5>: <RelationshipAssociation queryset #5>,
                        <Relationship instance #6>: <RelationshipAssociation queryset #6>,
                    },
                }`
        """
        src_relationships, dst_relationships = Relationship.objects.get_for_model(self)
        if advanced_ui is not None:
            src_relationships = src_relationships.filter(advanced_ui=advanced_ui)
            dst_relationships = dst_relationships.filter(advanced_ui=advanced_ui)
        content_type = ContentType.objects.get_for_model(self)

        sides = {
            RelationshipSideChoices.SIDE_SOURCE: src_relationships,
            RelationshipSideChoices.SIDE_DESTINATION: dst_relationships,
        }

        resp = {
            RelationshipSideChoices.SIDE_SOURCE: {},
            RelationshipSideChoices.SIDE_DESTINATION: {},
            RelationshipSideChoices.SIDE_PEER: {},
        }
        for side, relationships in sides.items():
            for relationship in relationships:
                if getattr(relationship, f"{side}_hidden") and not include_hidden:
                    continue

                # Determine if the relationship is applicable to this object based on the filter
                # To resolve the filter we are using the FilterSet for the given model
                # If there is no match when we query the primary key of the device along with the filter
                # Then the relationship is not applicable to this object
                if getattr(relationship, f"{side}_filter"):
                    filterset = get_filterset_for_model(self._meta.model)
                    if filterset:
                        filter_params = getattr(relationship, f"{side}_filter")
                        if not filterset(filter_params, self._meta.model.objects.filter(id=self.id)).qs.exists():
                            continue

                # Construct the queryset to query all RelationshipAssociation for this object and this relationship
                query_params = {"relationship": relationship}
                if not relationship.symmetric:
                    # Query for RelationshipAssociations that this object is on the expected side of
                    query_params[f"{side}_id"] = self.pk
                    query_params[f"{side}_type"] = content_type

                    resp[side][relationship] = RelationshipAssociation.objects.filter(**query_params)
                else:
                    # Query for RelationshipAssociations involving this object, regardless of side
                    resp[RelationshipSideChoices.SIDE_PEER][relationship] = RelationshipAssociation.objects.filter(
                        (
                            Q(source_id=self.pk, source_type=content_type)
                            | Q(destination_id=self.pk, destination_type=content_type)
                        ),
                        **query_params,
                    )

        return resp

### `get_relationships_data(**kwargs)`

Return a dictionary of relationships with the label and the value or the queryset for each.

Used for rendering relationships in the UI; see nautobot/core/templates/inc/relationships_table_rows.html

Returns:

Type | Description
--- | ---
`dict` | `{ "source": { <Relationship instance #1>: { # one-to-one relationship that self is the source of "label": "...", "peer_type": <ContentType>, "has_many": False, "value": <model instance>, # single destination for this relationship "url": "...", }, <Relationship instance #2>: { # one-to-many or many-to-many relationship that self is a source for "label": "...", "peer_type": <ContentType>, "has_many": True, "value": None, "queryset": <RelationshipAssociation queryset #2> # set of destinations for the relationship }, }, "destination": { (same format as "source" dict - relationships that self is the destination of) }, "peer": { (same format as "source" dict - symmetric relationships that self is involved in) }, }`

Source code in `nautobot/extras/models/relationships.py`

    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    def get_relationships_data(self, **kwargs):
        """
        Return a dictionary of relationships with the label and the value or the queryset for each.

        Used for rendering relationships in the UI; see nautobot/core/templates/inc/relationships_table_rows.html

        Returns:
            (dict): `{
                    "source": {
                        <Relationship instance #1>: {   # one-to-one relationship that self is the source of
                            "label": "...",
                            "peer_type": <ContentType>,
                            "has_many": False,
                            "value": <model instance>,     # single destination for this relationship
                            "url": "...",
                        },
                        <Relationship instance #2>: {   # one-to-many or many-to-many relationship that self is a source for
                            "label": "...",
                            "peer_type": <ContentType>,
                            "has_many": True,
                            "value": None,
                            "queryset": <RelationshipAssociation queryset #2>   # set of destinations for the relationship
                        },
                    },
                    "destination": {
                        (same format as "source" dict - relationships that self is the destination of)
                    },
                    "peer": {
                        (same format as "source" dict - symmetric relationships that self is involved in)
                    },
                }`
        """

        relationships_by_side = self.get_relationships(**kwargs)

        resp = {
            RelationshipSideChoices.SIDE_SOURCE: {},
            RelationshipSideChoices.SIDE_DESTINATION: {},
            RelationshipSideChoices.SIDE_PEER: {},
        }
        for side, relationships in relationships_by_side.items():
            for relationship, queryset in relationships.items():
                peer_side = RelationshipSideChoices.OPPOSITE[side]

                resp[side][relationship] = {
                    "label": relationship.get_label(side),
                    "value": None,
                }
                if not relationship.symmetric:
                    resp[side][relationship]["peer_type"] = getattr(relationship, f"{peer_side}_type")
                else:
                    # Symmetric relationship - source_type == destination_type, so it doesn't matter which we choose
                    resp[side][relationship]["peer_type"] = getattr(relationship, "source_type")

                resp[side][relationship]["has_many"] = relationship.has_many(peer_side)

                if resp[side][relationship]["has_many"]:
                    resp[side][relationship]["queryset"] = queryset
                else:
                    resp[side][relationship]["url"] = None
                    association = queryset.first()
                    if not association:
                        continue

                    peer = association.get_peer(self)

                    resp[side][relationship]["value"] = peer
                    if hasattr(peer, "get_absolute_url"):
                        resp[side][relationship]["url"] = peer.get_absolute_url()
                    else:
                        logger.warning("Peer object %s has no get_absolute_url() method", peer)

        return resp

### `get_relationships_data_advanced_fields()`

Same docstring as get_relationships_data() above except this only returns relationships where advanced_ui==True for displaying in the 'Advanced' tab on the object's page

Source code in `nautobot/extras/models/relationships.py`

    217
    218
    219
    220
    221
    222
    def get_relationships_data_advanced_fields(self):
        """
        Same docstring as get_relationships_data() above except this only returns relationships
        where advanced_ui==True for displaying in the 'Advanced' tab on the object's page
        """
        return self.get_relationships_data(advanced_ui=True)

### `get_relationships_data_basic_fields()`

Same docstring as get_relationships_data() above except this only returns relationships where advanced_ui==False for displaying in the main object detail tab on the object's page

Source code in `nautobot/extras/models/relationships.py`

    210
    211
    212
    213
    214
    215
    def get_relationships_data_basic_fields(self):
        """
        Same docstring as get_relationships_data() above except this only returns relationships
        where advanced_ui==False for displaying in the main object detail tab on the object's page
        """
        return self.get_relationships_data(advanced_ui=False)

### `required_related_objects_errors(output_for='ui', initial_data=None, relationships_key_specified=False, instance=None)` `classmethod`

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`output_for` | `str` | either "ui" or "api" depending on usage | `'ui'`
`initial_data` | `dict` | submitted form/serializer data to validate against | `None`
`relationships_key_specified` | `bool` | if the "relationships" key was provided or not | `False`
`instance` | `Optional[BaseModel]` | an optional model instance to validate against | `None`

Returns:

Type | Description
--- | ---
`list[dict]` | List of field error dicts if any are found

Source code in `nautobot/extras/models/relationships.py`

    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    @classmethod
    def required_related_objects_errors(
        cls, output_for="ui", initial_data=None, relationships_key_specified=False, instance=None
    ):
        """
        Args:
            output_for (str): either "ui" or "api" depending on usage
            initial_data (dict): submitted form/serializer data to validate against
            relationships_key_specified (bool): if the "relationships" key was provided or not
            instance (Optional[BaseModel]): an optional model instance to validate against
        Returns:
            (list[dict]): List of field error dicts if any are found
        """

        required_relationships = Relationship.objects.get_required_for_model(cls)
        relationships_field_errors = {}
        for relation in required_relationships:
            opposite_side = RelationshipSideChoices.OPPOSITE[relation.required_on]

            if relation.skip_required(cls, opposite_side):
                continue

            if relation.has_many(opposite_side):
                num_required_verbose = "at least one"
            else:
                num_required_verbose = "a"

            if output_for == "api":
                # If this is a model instance and the relationships json data key is missing, check to see if
                # required relationship associations already exist, and continue (ignore validation) if so
                if (
                    getattr(instance, "present_in_database", False) is True
                    and initial_data.get(relation, {}).get(opposite_side, {}) == {}
                    and not relationships_key_specified
                ):
                    filter_kwargs = {"relationship": relation, f"{relation.required_on}_id": instance.pk}
                    if RelationshipAssociation.objects.filter(**filter_kwargs).exists():
                        continue

            required_model_class = getattr(relation, f"{opposite_side}_type").model_class()
            required_model_meta = required_model_class._meta
            cr_field_name = f"cr_{relation.key}__{opposite_side}"
            name_plural = cls._meta.verbose_name_plural
            field_key = relation.key if output_for == "api" else cr_field_name
            field_errors = {field_key: []}

            if not required_model_class.objects.exists():
                hint = (
                    f"You need to create {num_required_verbose} {required_model_meta.verbose_name} "
                    f"before instantiating a {cls._meta.verbose_name}."
                )

                if output_for == "ui":
                    try:
                        add_url = reverse(get_route_for_model(required_model_class, "add"))
                        hint = format_html(
                            '<a target="_blank" href="{}">Click here</a> to create a {}.',
                            add_url,
                            required_model_meta.verbose_name,
                        )
                    except NoReverseMatch:
                        pass

                elif output_for == "api":
                    try:
                        api_post_url = reverse(get_route_for_model(required_model_class, "list", api=True))
                        hint = f"Create a {required_model_meta.verbose_name} by posting to {api_post_url}"
                    except NoReverseMatch:
                        pass

                error_message = format_html(
                    "{} require {} {}, but no {} exist yet. ",
                    bettertitle(name_plural),
                    num_required_verbose,
                    required_model_meta.verbose_name,
                    required_model_meta.verbose_name_plural,
                )
                error_message += hint
                field_errors[field_key].append(error_message)

            if initial_data is not None:
                supplied_data = []

                if output_for == "ui":
                    supplied_data = initial_data.get(field_key, [])

                elif output_for == "api":
                    supplied_data = initial_data.get(relation, {}).get(opposite_side, {})

                if not supplied_data:
                    if output_for == "ui":
                        field_errors[field_key].append(
                            f"You need to select {num_required_verbose} {required_model_meta.verbose_name}."
                        )
                    elif output_for == "api":
                        field_errors[field_key].append(
                            f'You need to specify ["relationships"]["{relation.key}"]["{opposite_side}"]["objects"].'
                        )

            if len(field_errors[field_key]) > 0:
                relationships_field_errors[field_key] = field_errors[field_key]

        return relationships_field_errors

## `nautobot.apps.models.RestrictedQuerySet`

Bases: `CompositeKeyQuerySetMixin`, `QuerySet`

Source code in `nautobot/core/models/querysets.py`

    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    class RestrictedQuerySet(CompositeKeyQuerySetMixin, QuerySet):
        def restrict(self, user, action="view"):
            """
            Filter the QuerySet to return only objects on which the specified user has been granted the specified
            permission.

            :param user: User instance
            :param action: The action which must be permitted (e.g. "view" for "dcim.view_location"); default is 'view'
            """
            # Resolve the full name of the required permission
            app_label = self.model._meta.app_label
            model_name = self.model._meta.model_name
            permission_required = f"{app_label}.{action}_{model_name}"

            # Bypass restriction for superusers and exempt views
            if user.is_superuser or permissions.permission_is_exempt(permission_required):
                qs = self

            # User is anonymous or has not been granted the requisite permission
            elif not user.is_authenticated or permission_required not in user.get_all_permissions():
                qs = self.none()

            # Filter the queryset to include only objects with allowed attributes
            else:
                attrs = Q()
                for perm_attrs in user._object_perm_cache[permission_required]:
                    if isinstance(perm_attrs, list):
                        for p in perm_attrs:
                            attrs |= Q(**p)
                    elif perm_attrs:
                        attrs |= Q(**perm_attrs)
                    else:
                        # Any permission with null constraints grants access to _all_ instances
                        attrs = Q()
                        break
                qs = self.filter(attrs)

            return qs

        def check_perms(self, user, *, instance=None, pk=None, action="view"):
            """
            Check whether the given user can perform the given action with regard to the given instance of this model.

            Either instance or pk must be specified, but not both.

            Args:
              user (User): User instance
              instance (self.model): Instance of this queryset's model to check, if pk is not provided
              pk (uuid): Primary key of the desired instance to check for, if instance is not provided
              action (str): The action which must be permitted (e.g. "view" for "dcim.view_location"); default is 'view'

            Returns:
                (bool): Whether the action is permitted or not
            """
            if instance is not None and pk is not None and instance.pk != pk:
                raise RuntimeError("Should not be called with both instance and pk specified!")
            if instance is None and pk is None:
                raise ValueError("Either instance or pk must be specified!")
            if instance is not None and not isinstance(instance, self.model):
                raise TypeError(f"{instance} is not a {self.model}")
            if pk is None:
                pk = instance.pk

            return self.restrict(user, action).filter(pk=pk).exists()

        def distinct_values_list(self, *fields, flat=False, named=False):
            """Wrapper for `QuerySet.values_list()` that adds the `distinct()` query to return a list of unique values.

            Note:
                Uses `QuerySet.order_by()` to disable ordering, preventing unexpected behavior when using `values_list` described
                in the Django `distinct()` documentation at https://docs.djangoproject.com/en/stable/ref/models/querysets/#distinct

            Args:
                *fields (str): Optional positional arguments which specify field names.
                flat (bool): Set to True to return a QuerySet of individual values instead of a QuerySet of tuples.
                    Defaults to False.
                named (bool): Set to True to return a QuerySet of namedtuples. Defaults to False.

            Returns:
                (QuerySet): A QuerySet of tuples or, if `flat` is set to True, a queryset of individual values.

            """
            return self.order_by().values_list(*fields, flat=flat, named=named).distinct()

### `check_perms(user, *, instance=None, pk=None, action='view')`

Check whether the given user can perform the given action with regard to the given instance of this model.

Either instance or pk must be specified, but not both.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`user` | `User` | User instance | required
`instance` | `self.model` | Instance of this queryset's model to check, if pk is not provided | `None`
`pk` | `uuid` | Primary key of the desired instance to check for, if instance is not provided | `None`
`action` | `str` | The action which must be permitted (e.g. "view" for "dcim.view_location"); default is 'view' | `'view'`

Returns:

Type | Description
--- | ---
`bool` | Whether the action is permitted or not

Source code in `nautobot/core/models/querysets.py`

    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    def check_perms(self, user, *, instance=None, pk=None, action="view"):
        """
        Check whether the given user can perform the given action with regard to the given instance of this model.

        Either instance or pk must be specified, but not both.

        Args:
          user (User): User instance
          instance (self.model): Instance of this queryset's model to check, if pk is not provided
          pk (uuid): Primary key of the desired instance to check for, if instance is not provided
          action (str): The action which must be permitted (e.g. "view" for "dcim.view_location"); default is 'view'

        Returns:
            (bool): Whether the action is permitted or not
        """
        if instance is not None and pk is not None and instance.pk != pk:
            raise RuntimeError("Should not be called with both instance and pk specified!")
        if instance is None and pk is None:
            raise ValueError("Either instance or pk must be specified!")
        if instance is not None and not isinstance(instance, self.model):
            raise TypeError(f"{instance} is not a {self.model}")
        if pk is None:
            pk = instance.pk

        return self.restrict(user, action).filter(pk=pk).exists()

### `distinct_values_list(*fields, flat=False, named=False)`

Wrapper for `QuerySet.values_list()` that adds the `distinct()` query to return a list of unique values.

Note

Uses `QuerySet.order_by()` to disable ordering, preventing unexpected behavior when using `values_list` described in the Django `distinct()` documentation at https://docs.djangoproject.com/en/stable/ref/models/querysets/#distinct

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`*fields` | `str` | Optional positional arguments which specify field names. | `()`
`flat` | `bool` | Set to True to return a QuerySet of individual values instead of a QuerySet of tuples. Defaults to False. | `False`
`named` | `bool` | Set to True to return a QuerySet of namedtuples. Defaults to False. | `False`

Returns:

Type | Description
--- | ---
`QuerySet` | A QuerySet of tuples or, if `flat` is set to True, a queryset of individual values.

Source code in `nautobot/core/models/querysets.py`

    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    def distinct_values_list(self, *fields, flat=False, named=False):
        """Wrapper for `QuerySet.values_list()` that adds the `distinct()` query to return a list of unique values.

        Note:
            Uses `QuerySet.order_by()` to disable ordering, preventing unexpected behavior when using `values_list` described
            in the Django `distinct()` documentation at https://docs.djangoproject.com/en/stable/ref/models/querysets/#distinct

        Args:
            *fields (str): Optional positional arguments which specify field names.
            flat (bool): Set to True to return a QuerySet of individual values instead of a QuerySet of tuples.
                Defaults to False.
            named (bool): Set to True to return a QuerySet of namedtuples. Defaults to False.

        Returns:
            (QuerySet): A QuerySet of tuples or, if `flat` is set to True, a queryset of individual values.

        """
        return self.order_by().values_list(*fields, flat=flat, named=named).distinct()

### `restrict(user, action='view')`

Filter the QuerySet to return only objects on which the specified user has been granted the specified permission.

:param user: User instance :param action: The action which must be permitted (e.g. "view" for "dcim.view_location"); default is 'view'

Source code in `nautobot/core/models/querysets.py`

    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    def restrict(self, user, action="view"):
        """
        Filter the QuerySet to return only objects on which the specified user has been granted the specified
        permission.

        :param user: User instance
        :param action: The action which must be permitted (e.g. "view" for "dcim.view_location"); default is 'view'
        """
        # Resolve the full name of the required permission
        app_label = self.model._meta.app_label
        model_name = self.model._meta.model_name
        permission_required = f"{app_label}.{action}_{model_name}"

        # Bypass restriction for superusers and exempt views
        if user.is_superuser or permissions.permission_is_exempt(permission_required):
            qs = self

        # User is anonymous or has not been granted the requisite permission
        elif not user.is_authenticated or permission_required not in user.get_all_permissions():
            qs = self.none()

        # Filter the queryset to include only objects with allowed attributes
        else:
            attrs = Q()
            for perm_attrs in user._object_perm_cache[permission_required]:
                if isinstance(perm_attrs, list):
                    for p in perm_attrs:
                        attrs |= Q(**p)
                elif perm_attrs:
                    attrs |= Q(**perm_attrs)
                else:
                    # Any permission with null constraints grants access to _all_ instances
                    attrs = Q()
                    break
            qs = self.filter(attrs)

        return qs

## `nautobot.apps.models.StatusField`

Bases: `ForeignKeyLimitedByContentTypes`

Model database field that automatically limits custom choices.

The limit_choices_to for the field are automatically derived from

- the content-type to which the field is attached (e.g. `dcim.device`)

Source code in `nautobot/extras/models/statuses.py`

     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
    class StatusField(ForeignKeyLimitedByContentTypes):
        """
        Model database field that automatically limits custom choices.

        The limit_choices_to for the field are automatically derived from:

            - the content-type to which the field is attached (e.g. `dcim.device`)
        """

        def __init__(self, *args, **kwargs):
            kwargs.setdefault("to", Status)
            kwargs.setdefault("on_delete", models.PROTECT)
            super().__init__(*args, **kwargs)

        def contribute_to_class(self, cls, *args, **kwargs):
            """
            Overload default so that we can assert that `.get_FOO_display` is
            attached to any model that is using a `StatusField`.

            Using `.contribute_to_class()` is how field objects get added to the model
            at during the instance preparation. This is also where any custom model
            methods are hooked in. So in short this method asserts that any time a
            `StatusField` is added to a model, that model also gets a
            `.get_status_display()` and a `.get_status_color()` method without
            having to define it on the model yourself.
            """
            super().contribute_to_class(cls, *args, **kwargs)

            def _get_FIELD_display(self, field):
                """
                Closure to replace default model method of the same name.

                Cargo-culted from `django.db.models.base.Model._get_FIELD_display`
                """
                choices = field.get_choices()
                value = getattr(self, field.attname)
                choices_dict = dict(make_hashable(choices))
                # force_str() to coerce lazy strings.
                return force_str(choices_dict.get(make_hashable(value), value), strings_only=True)

            # Install `.get_FOO_display()` onto the model using our own version.
            if f"get_{self.name}_display" not in cls.__dict__:
                setattr(
                    cls,
                    f"get_{self.name}_display",
                    partialmethod(_get_FIELD_display, field=self),
                )

            def _get_FIELD_color(self, field):
                """
                Return `self.FOO.color` (where FOO is field name).

                I am added to the model via `StatusField.contribute_to_class()`.
                """
                field_method = getattr(self, field.name)
                return getattr(field_method, "color")

            # Install `.get_FOO_color()` onto the model using our own version.
            if f"get_{self.name}_color" not in cls.__dict__:
                setattr(
                    cls,
                    f"get_{self.name}_color",
                    partialmethod(_get_FIELD_color, field=self),
                )

### `contribute_to_class(cls, *args, **kwargs)`

Overload default so that we can assert that `.get_FOO_display` is attached to any model that is using a `StatusField`.

Using `.contribute_to_class()` is how field objects get added to the model at during the instance preparation. This is also where any custom model methods are hooked in. So in short this method asserts that any time a `StatusField` is added to a model, that model also gets a `.get_status_display()` and a `.get_status_color()` method without having to define it on the model yourself.

Source code in `nautobot/extras/models/statuses.py`

     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
    def contribute_to_class(self, cls, *args, **kwargs):
        """
        Overload default so that we can assert that `.get_FOO_display` is
        attached to any model that is using a `StatusField`.

        Using `.contribute_to_class()` is how field objects get added to the model
        at during the instance preparation. This is also where any custom model
        methods are hooked in. So in short this method asserts that any time a
        `StatusField` is added to a model, that model also gets a
        `.get_status_display()` and a `.get_status_color()` method without
        having to define it on the model yourself.
        """
        super().contribute_to_class(cls, *args, **kwargs)

        def _get_FIELD_display(self, field):
            """
            Closure to replace default model method of the same name.

            Cargo-culted from `django.db.models.base.Model._get_FIELD_display`
            """
            choices = field.get_choices()
            value = getattr(self, field.attname)
            choices_dict = dict(make_hashable(choices))
            # force_str() to coerce lazy strings.
            return force_str(choices_dict.get(make_hashable(value), value), strings_only=True)

        # Install `.get_FOO_display()` onto the model using our own version.
        if f"get_{self.name}_display" not in cls.__dict__:
            setattr(
                cls,
                f"get_{self.name}_display",
                partialmethod(_get_FIELD_display, field=self),
            )

        def _get_FIELD_color(self, field):
            """
            Return `self.FOO.color` (where FOO is field name).

            I am added to the model via `StatusField.contribute_to_class()`.
            """
            field_method = getattr(self, field.name)
            return getattr(field_method, "color")

        # Install `.get_FOO_color()` onto the model using our own version.
        if f"get_{self.name}_color" not in cls.__dict__:
            setattr(
                cls,
                f"get_{self.name}_color",
                partialmethod(_get_FIELD_color, field=self),
            )

## `nautobot.apps.models.StatusModel`

Bases: `models.Model`

Deprecated abstract base class for any model which may have statuses.

Just directly include a StatusField instead for any new models.

Source code in `nautobot/extras/models/statuses.py`

    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    @class_deprecated(message="please directly declare `status = StatusField(...)` on your model instead")
    class StatusModel(models.Model):
        """
        Deprecated abstract base class for any model which may have statuses.

        Just directly include a StatusField instead for any new models.
        """

        status = StatusField(null=True)  # for backward compatibility

        class Meta:
            abstract = True

## `nautobot.apps.models.TagsField`

Bases: `TaggableManager`

Override FormField method on taggit.managers.TaggableManager to match the Nautobot UI.

Source code in `nautobot/core/models/fields.py`

    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    class TagsField(TaggableManager):
        """Override FormField method on taggit.managers.TaggableManager to match the Nautobot UI."""

        def __init__(self, *args, **kwargs):
            from nautobot.extras.models.tags import TaggedItem

            kwargs.setdefault("through", TaggedItem)
            kwargs.setdefault("manager", TagsManager)
            kwargs.setdefault("ordering", ["name"])
            super().__init__(*args, **kwargs)

        def formfield(self, form_class=fields.DynamicModelMultipleChoiceField, **kwargs):
            from nautobot.extras.models.tags import Tag

            queryset = Tag.objects.get_for_model(self.model)
            kwargs.setdefault("queryset", queryset)
            kwargs.setdefault("required", False)
            kwargs.setdefault("query_params", {"content_types": self.model._meta.label_lower})
            return super().formfield(form_class=form_class, **kwargs)

## `nautobot.apps.models.TagsManager`

Bases: `_TaggableManager`, `BaseManager`

Manager class for model 'tags' fields.

Source code in `nautobot/core/models/managers.py`

    43
    44
    class TagsManager(_TaggableManager, BaseManager):
        """Manager class for model 'tags' fields."""

## `nautobot.apps.models.TreeManager`

Bases: `TreeManager_`, `BaseManager.from_queryset(TreeQuerySet)`

Extend django-tree-queries' TreeManager to incorporate RestrictedQuerySet.

Source code in `nautobot/core/models/tree_queries.py`

    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    class TreeManager(TreeManager_, BaseManager.from_queryset(TreeQuerySet)):
        """
        Extend django-tree-queries' TreeManager to incorporate RestrictedQuerySet.
        """

        _with_tree_fields = True
        use_in_migrations = True

        @property
        def max_depth_cache_key(self):
            return f"nautobot.{self.model._meta.concrete_model._meta.label_lower}.max_depth"

        @property
        def max_depth(self):
            """Cacheable version of `TreeQuerySet.max_tree_depth()`.

            Generally TreeManagers are persistent objects while TreeQuerySets are not, hence the difference in behavior.
            """
            max_depth = cache.get(self.max_depth_cache_key)
            if max_depth is None:
                max_depth = self.max_tree_depth()
                cache.set(self.max_depth_cache_key, max_depth)
            return max_depth

### `max_depth` `property`

Cacheable version of `TreeQuerySet.max_tree_depth()`.

Generally TreeManagers are persistent objects while TreeQuerySets are not, hence the difference in behavior.

## `nautobot.apps.models.TreeModel`

Bases: `TreeNode`

Nautobot-specific base class for models that exist in a self-referential tree.

Source code in `nautobot/core/models/tree_queries.py`

     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
    class TreeModel(TreeNode):
        """
        Nautobot-specific base class for models that exist in a self-referential tree.
        """

        objects = TreeManager()

        class Meta:
            abstract = True

        @property
        def display(self):
            """
            By default, TreeModels display their full ancestry for clarity.

            As this is an expensive thing to calculate, we cache it for a few seconds in the case of repeated lookups.
            """
            if not hasattr(self, "name"):
                raise NotImplementedError("default TreeModel.display implementation requires a `name` attribute!")
            cache_key = f"nautobot.{self._meta.concrete_model._meta.label_lower}.{self.id}.display"
            display_str = cache.get(cache_key, "")
            if display_str:
                return display_str
            try:
                if self.parent is not None:
                    display_str = self.parent.display + " → "
            except self.DoesNotExist:
                # Expected to occur at times during bulk-delete operations
                pass
            finally:
                display_str += self.name
                cache.set(cache_key, display_str, 5)
                return display_str  # pylint: disable=lost-exception

### `display` `property`

By default, TreeModels display their full ancestry for clarity.

As this is an expensive thing to calculate, we cache it for a few seconds in the case of repeated lookups.

## `nautobot.apps.models.TreeQuerySet`

Bases: `TreeQuerySet_`, `querysets.RestrictedQuerySet`

Combine django-tree-queries' TreeQuerySet with our RestrictedQuerySet for permissions enforcement.

Source code in `nautobot/core/models/tree_queries.py`

     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    class TreeQuerySet(TreeQuerySet_, querysets.RestrictedQuerySet):
        """
        Combine django-tree-queries' TreeQuerySet with our RestrictedQuerySet for permissions enforcement.
        """

        def ancestors(self, of, *, include_self=False):
            """Custom ancestors method for optimization purposes.

            Dynamically computes ancestors either through the tree or through the `parent` foreign key depending on whether
            tree fields are present on `of`.
            """
            # If `of` has `tree_depth` defined, i.e. if it was retrieved from the database on a queryset where tree fields
            # were enabled (see `TreeQuerySet.with_tree_fields` and `TreeQuerySet.without_tree_fields`), use the default
            # implementation from `tree_queries.query.TreeQuerySet`.
            # Furthermore, if `of` doesn't have a parent field we also have to defer to the tree-based implementation which
            # will then annotate the tree fields and proceed as usual.
            if hasattr(of, "tree_depth") or not hasattr(of, "parent"):
                return super().ancestors(of, include_self=include_self)
            # In the other case, traverse the `parent` foreign key until the root.
            model_class = of._meta.concrete_model
            ancestor_pks = []
            if include_self:
                ancestor_pks.append(of.pk)
            while of := of.parent:
                # Insert in reverse order so that the root is the first element
                ancestor_pks.insert(0, of.pk)
            # Maintain API compatibility by returning a queryset instead of a list directly.
            # Reference:
            # https://stackoverflow.com/questions/4916851/django-get-a-queryset-from-array-of-ids-in-specific-order
            preserve_order = Case(*[When(pk=pk, then=position) for position, pk in enumerate(ancestor_pks)])
            return model_class.objects.without_tree_fields().filter(pk__in=ancestor_pks).order_by(preserve_order)

        def max_tree_depth(self):
            r"""
            Get the maximum tree depth of any node in this queryset.

            In most cases you should use TreeManager.max_depth instead as it's cached and this is not.

            root  - depth 0
             \
              branch  - depth 1
                \
                leaf  - depth 2

            Note that a queryset with only root nodes will return zero, and an empty queryset will also return zero.
            This is probably a bug, we should really return -1 in the case of an empty queryset, but this is
            "working as implemented" and changing it would possibly be a breaking change at this point.
            """
            deepest = self.with_tree_fields().extra(order_by=["-__tree.tree_depth"]).first()
            if deepest is not None:
                return deepest.tree_depth
            return 0

### `ancestors(of, *, include_self=False)`

Custom ancestors method for optimization purposes.

Dynamically computes ancestors either through the tree or through the `parent` foreign key depending on whether tree fields are present on `of`.

Source code in `nautobot/core/models/tree_queries.py`

    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    def ancestors(self, of, *, include_self=False):
        """Custom ancestors method for optimization purposes.

        Dynamically computes ancestors either through the tree or through the `parent` foreign key depending on whether
        tree fields are present on `of`.
        """
        # If `of` has `tree_depth` defined, i.e. if it was retrieved from the database on a queryset where tree fields
        # were enabled (see `TreeQuerySet.with_tree_fields` and `TreeQuerySet.without_tree_fields`), use the default
        # implementation from `tree_queries.query.TreeQuerySet`.
        # Furthermore, if `of` doesn't have a parent field we also have to defer to the tree-based implementation which
        # will then annotate the tree fields and proceed as usual.
        if hasattr(of, "tree_depth") or not hasattr(of, "parent"):
            return super().ancestors(of, include_self=include_self)
        # In the other case, traverse the `parent` foreign key until the root.
        model_class = of._meta.concrete_model
        ancestor_pks = []
        if include_self:
            ancestor_pks.append(of.pk)
        while of := of.parent:
            # Insert in reverse order so that the root is the first element
            ancestor_pks.insert(0, of.pk)
        # Maintain API compatibility by returning a queryset instead of a list directly.
        # Reference:
        # https://stackoverflow.com/questions/4916851/django-get-a-queryset-from-array-of-ids-in-specific-order
        preserve_order = Case(*[When(pk=pk, then=position) for position, pk in enumerate(ancestor_pks)])
        return model_class.objects.without_tree_fields().filter(pk__in=ancestor_pks).order_by(preserve_order)

### `max_tree_depth()`

Get the maximum tree depth of any node in this queryset.

In most cases you should use TreeManager.max_depth instead as it's cached and this is not.

root - depth 0 \ branch - depth 1 \ leaf - depth 2

Note that a queryset with only root nodes will return zero, and an empty queryset will also return zero. This is probably a bug, we should really return -1 in the case of an empty queryset, but this is "working as implemented" and changing it would possibly be a breaking change at this point.

Source code in `nautobot/core/models/tree_queries.py`

    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    def max_tree_depth(self):
        r"""
        Get the maximum tree depth of any node in this queryset.

        In most cases you should use TreeManager.max_depth instead as it's cached and this is not.

        root  - depth 0
         \
          branch  - depth 1
            \
            leaf  - depth 2

        Note that a queryset with only root nodes will return zero, and an empty queryset will also return zero.
        This is probably a bug, we should really return -1 in the case of an empty queryset, but this is
        "working as implemented" and changing it would possibly be a breaking change at this point.
        """
        deepest = self.with_tree_fields().extra(order_by=["-__tree.tree_depth"]).first()
        if deepest is not None:
            return deepest.tree_depth
        return 0

## `nautobot.apps.models.ValidRegexValidator`

Bases: `RegexValidator`

Checks that the value is a valid regular expression.

Don't confuse this with `RegexValidator`, which uses a regex to validate a value.

Source code in `nautobot/core/models/validators.py`

    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    class ValidRegexValidator(RegexValidator):
        """
        Checks that the value is a valid regular expression.

        Don't confuse this with `RegexValidator`, which *uses* a regex to validate a value.
        """

        message = "%(value)r is not a valid regular expression."
        code = "regex_invalid"

        def __call__(self, value):
            try:
                return re.compile(value)
            except (re.error, TypeError):
                raise ValidationError(self.message, code=self.code, params={"value": value})

## `nautobot.apps.models.VarbinaryIPField`

Bases: `models.BinaryField`

IP network address

Source code in `nautobot/ipam/fields.py`

     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    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
    class VarbinaryIPField(models.BinaryField):
        """
        IP network address
        """

        description = "IP network address"

        def db_type(self, connection):
            """Returns the correct field type for a given database vendor."""

            # Use 'bytea' type for PostgreSQL.
            if connection.vendor == "postgresql":
                return "bytea"

            # Or 'varbinary' for everyone else.
            return "varbinary(16)"

        def value_to_string(self, obj):
            """IPField is serialized as str(IPAddress())"""
            value = self.value_from_object(obj)
            if not value:
                return value

            return str(self._parse_address(value))

        def _parse_address(self, value):
            """
            Parse `str`, `bytes` (varbinary), or `netaddr.IPAddress to `netaddr.IPAddress`.
            """
            try:
                int_value = int.from_bytes(value, "big")
                # Distinguish between
                # \x00\x00\x00\x01 (IPv4 0.0.0.1) and
                # \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 (IPv6 ::1), among other cases
                version = 4 if len(value) == 4 else 6
                value = int_value
            except TypeError:
                version = None  # It's a string, IP version should be self-evident

            try:
                return netaddr.IPAddress(value, version=version)
            except netaddr.AddrFormatError:
                raise ValidationError(f"Invalid IP address format: {value}")
            except (TypeError, ValueError) as e:
                raise ValidationError(e)

        def from_db_value(self, value, expression, connection):
            """Converts DB (varbinary) to Python (str)."""
            return self.to_python(value)

        def to_python(self, value):
            """Converts `value` to Python (str)."""
            if isinstance(value, netaddr.IPAddress):
                return str(value)

            if value is None:
                return value

            return str(self._parse_address(value))

        def get_db_prep_value(self, value, connection, prepared=False):
            """Converts Python (str) to DB (varbinary)."""
            if value is None:
                return value

            # Parse the address and then pack it to binary.
            value = self._parse_address(value).packed

            # Use defaults for PostgreSQL
            if connection.vendor == "postgresql":
                return super().get_db_prep_value(value, connection, prepared)

            return value

        def form_class(self):
            return IPNetworkFormField

        def formfield(self, *args, **kwargs):
            defaults = {"form_class": self.form_class()}
            defaults.update(kwargs)
            return super().formfield(*args, **defaults)

        def get_default(self):
            value = super().get_default()
            # Prevent None or "" values from being represented as b''
            return None if value in self.empty_values else value

### `db_type(connection)`

Returns the correct field type for a given database vendor.

Source code in `nautobot/ipam/fields.py`

    16
    17
    18
    19
    20
    21
    22
    23
    24
    def db_type(self, connection):
        """Returns the correct field type for a given database vendor."""

        # Use 'bytea' type for PostgreSQL.
        if connection.vendor == "postgresql":
            return "bytea"

        # Or 'varbinary' for everyone else.
        return "varbinary(16)"

### `from_db_value(value, expression, connection)`

Converts DB (varbinary) to Python (str).

Source code in `nautobot/ipam/fields.py`

    55
    56
    57
    def from_db_value(self, value, expression, connection):
        """Converts DB (varbinary) to Python (str)."""
        return self.to_python(value)

### `get_db_prep_value(value, connection, prepared=False)`

Converts Python (str) to DB (varbinary).

Source code in `nautobot/ipam/fields.py`

    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    def get_db_prep_value(self, value, connection, prepared=False):
        """Converts Python (str) to DB (varbinary)."""
        if value is None:
            return value

        # Parse the address and then pack it to binary.
        value = self._parse_address(value).packed

        # Use defaults for PostgreSQL
        if connection.vendor == "postgresql":
            return super().get_db_prep_value(value, connection, prepared)

        return value

### `to_python(value)`

Converts `value` to Python (str).

Source code in `nautobot/ipam/fields.py`

    59
    60
    61
    62
    63
    64
    65
    66
    67
    def to_python(self, value):
        """Converts `value` to Python (str)."""
        if isinstance(value, netaddr.IPAddress):
            return str(value)

        if value is None:
            return value

        return str(self._parse_address(value))

### `value_to_string(obj)`

IPField is serialized as str(IPAddress())

Source code in `nautobot/ipam/fields.py`

    26
    27
    28
    29
    30
    31
    32
    def value_to_string(self, obj):
        """IPField is serialized as str(IPAddress())"""
        value = self.value_from_object(obj)
        if not value:
            return value

        return str(self._parse_address(value))

## `nautobot.apps.models.array_to_string(array)`

Generate an efficient, human-friendly string from a set of integers. Intended for use with ArrayField.

For example

[0, 1, 2, 10, 14, 15, 16] => "0-2, 10, 14-16"

Source code in `nautobot/core/models/utils.py`

    17
    18
    19
    20
    21
    22
    23
    24
    def array_to_string(array):
        """
        Generate an efficient, human-friendly string from a set of integers. Intended for use with ArrayField.
        For example:
            [0, 1, 2, 10, 14, 15, 16] => "0-2, 10, 14-16"
        """
        group = (list(x) for _, x in groupby(sorted(array), lambda x, c=count(): next(c) - x))
        return ", ".join("-".join(map(str, (g[0], g[-1])[: len(g)])) for g in group)

## `nautobot.apps.models.construct_composite_key(values)`

Convert the given list of natural key values to a single URL-path-usable string.

- Non-URL-safe characters are percent-encoded.
- Null (`None`) values are percent-encoded as a literal null character `%00`.

Reversible by `deconstruct_composite_key()`.

Source code in `nautobot/core/models/utils.py`

    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    def construct_composite_key(values):
        """
        Convert the given list of natural key values to a single URL-path-usable string.

        - Non-URL-safe characters are percent-encoded.
        - Null (`None`) values are percent-encoded as a literal null character `%00`.

        Reversible by `deconstruct_composite_key()`.
        """
        values = [str(value) if value is not None else "\0" for value in values]
        # . and : are generally "safe enough" to use in URL parameters, and are common in some natural key fields,
        # so we don't quote them by default (although `deconstruct_composite_key` will work just fine if you do!)
        # / is a bit trickier to handle in URL paths, so for now we *do* quote it, even though it appears in IPAddress, etc.
        values = constants.COMPOSITE_KEY_SEPARATOR.join(quote_plus(value, safe=".:") for value in values)
        return values

## `nautobot.apps.models.construct_natural_slug(values, pk=None)`

Convert the given list of natural key `values` to a single human-readable string.

If `pk` is provided, it will be appended to the end of the natural slug. If the PK is a UUID, only the first four characters will be appended.

A third-party lossy `slugify()` function is used to convert each natural key value to a slug, and then they are joined with an underscore.

- Spaces or repeated dashes are converted to single dashes.
- Accents and ligatures from Unicode characters are reduced to ASCII.
- Remove remaining characters that are not alphanumerics, underscores, or hyphens.
- Converted to lowercase.
- Strips leading/trailing whitespace, dashes, and underscores.
- Each natural key value in the list is separated by underscores.
- Emojis will be converted to their registered name.

This value is not reversible, is lossy, and is not guaranteed to be unique.

Source code in `nautobot/core/models/utils.py`

    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
    def construct_natural_slug(values, pk=None):
        """
        Convert the given list of natural key `values` to a single human-readable string.

        If `pk` is provided, it will be appended to the end of the natural slug. If the PK is a UUID,
        only the first four characters will be appended.

        A third-party lossy `slugify()` function is used to convert each natural key value to a
        slug, and then they are joined with an underscore.

        - Spaces or repeated dashes are converted to single dashes.
        - Accents and ligatures from Unicode characters are reduced to ASCII.
        - Remove remaining characters that are not alphanumerics, underscores, or hyphens.
        - Converted to lowercase.
        - Strips leading/trailing whitespace, dashes, and underscores.
        - Each natural key value in the list is separated by underscores.
        - Emojis will be converted to their registered name.

        This value is not reversible, is lossy, and is not guaranteed to be unique.
        """
        # In some cases the natural key might not be a list.
        if isinstance(values, tuple):
            values = list(values)

        # If a pk is passed through, append it to the values.
        if pk is not None:
            pk = str(pk)
            # Keep the first 4 characters of the UUID.
            if is_uuid(pk):
                pk = pk[:4]
            values.append(pk)

        values = (str(value) if value is not None else "\0" for value in values)
        # Replace any emojis with their string name, and then slugify that.
        values = (slugify(emoji.replace_emoji(value, unicodedata.name)) for value in values)
        return constants.NATURAL_SLUG_SEPARATOR.join(values)

## `nautobot.apps.models.count_related(model, field, *, filter_dict=None, distinct=False)`

Return a Subquery suitable for annotating a child object count.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`model` | `Model` | The related model to aggregate | required
`field` | `str` | The field on the related model which points back to the OuterRef model | required
`filter_dict` | `dict` | Optional dict of filter key/value pairs to limit the Subquery | `None`

Source code in `nautobot/core/models/querysets.py`

     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    def count_related(model, field, *, filter_dict=None, distinct=False):
        """
        Return a Subquery suitable for annotating a child object count.

        Args:
            model (Model): The related model to aggregate
            field (str): The field on the related model which points back to the OuterRef model
            filter_dict (dict): Optional dict of filter key/value pairs to limit the Subquery
        """
        filters = {field: OuterRef("pk")}
        if filter_dict:
            filters.update(filter_dict)

        manager = model.objects
        if hasattr(model.objects, "without_tree_fields"):
            manager = manager.without_tree_fields()
        qs = manager.filter(**filters).order_by().values(field)
        if distinct:
            qs = qs.annotate(c=Count("pk", distinct=distinct)).values("c")
        else:
            qs = qs.annotate(c=Count("*")).values("c")
        subquery = Subquery(qs)

        return Coalesce(subquery, 0)

## `nautobot.apps.models.deconstruct_composite_key(composite_key)`

Convert the given composite-key string back to a list of distinct values.

- Percent-encoded characters are converted back to their raw values
- Single literal null characters `%00` are converted back to a Python `None`.

Inverse operation of `construct_composite_key()`.

Source code in `nautobot/core/models/utils.py`

    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    def deconstruct_composite_key(composite_key):
        """
        Convert the given composite-key string back to a list of distinct values.

        - Percent-encoded characters are converted back to their raw values
        - Single literal null characters `%00` are converted back to a Python `None`.

        Inverse operation of `construct_composite_key()`.
        """
        values = [unquote_plus(value) for value in composite_key.split(constants.COMPOSITE_KEY_SEPARATOR)]
        values = [value if value != "\0" else None for value in values]
        return values

## `nautobot.apps.models.extras_features(*features)`

Decorator used to register extras provided features to a model

Source code in `nautobot/extras/utils.py`

    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    def extras_features(*features):
        """
        Decorator used to register extras provided features to a model
        """

        def wrapper(model_class):
            # Initialize the model_features store if not already defined
            if "model_features" not in registry:
                registry["model_features"] = {f: collections.defaultdict(list) for f in EXTRAS_FEATURES}
            for feature in features:
                if feature in EXTRAS_FEATURES:
                    app_label, model_name = model_class._meta.label_lower.split(".")
                    registry["model_features"][feature][app_label].append(model_name)
                else:
                    raise ValueError(f"{feature} is not a valid extras feature!")
            return model_class

        return wrapper

## `nautobot.apps.models.find_models_with_matching_fields(app_models, field_names, field_attributes=None)`

Find all models that have fields with the specified names, and return them grouped by app.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`app_models` | `list[BaseModel]` | A list of model classes to search through. | required
`field_names` | `list[str]` | A list of names of fields that must be present in order for the model to be considered | required
`field_attributes` | `dict` | Optional dictionary of attributes to filter the fields by. | `None`

Return

(dict): A dictionary where the keys are app labels and the values are sets of model names.

Source code in `nautobot/core/models/utils.py`

    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    def find_models_with_matching_fields(app_models, field_names, field_attributes=None):
        """
        Find all models that have fields with the specified names, and return them grouped by app.

        Args:
            app_models (list[BaseModel]): A list of model classes to search through.
            field_names (list[str]): A list of names of fields that must be present in order for the model to be considered
            field_attributes (dict): Optional dictionary of attributes to filter the fields by.

        Return:
            (dict): A dictionary where the keys are app labels and the values are sets of model names.
        """
        registry_items = {}
        field_attributes = field_attributes or {}
        for model_class in app_models:
            app_label, model_name = model_class._meta.label_lower.split(".")
            for field_name in field_names:
                try:
                    field = model_class._meta.get_field(field_name)
                    if all((getattr(field, item, None) == value for item, value in field_attributes.items())):
                        registry_items.setdefault(app_label, set()).add(model_name)
                except FieldDoesNotExist:
                    pass
        registry_items = {key: sorted(value) for key, value in registry_items.items()}
        return registry_items

## `nautobot.apps.models.get_all_concrete_models(base_class)`

Get a list of all non-abstract models that inherit from the given base_class.

Source code in `nautobot/core/models/utils.py`

    27
    28
    29
    30
    31
    32
    33
    34
    def get_all_concrete_models(base_class):
        """Get a list of all non-abstract models that inherit from the given base_class."""
        models = []
        for appconfig in apps.get_app_configs():
            for model in appconfig.get_models():
                if issubclass(model, base_class) and not model._meta.abstract:
                    models.append(model)
        return sorted(models, key=lambda model: (model._meta.app_label, model._meta.model_name))

## `nautobot.apps.models.get_default_namespace()`

Return the Global namespace.

Source code in `nautobot/ipam/models.py`

    76
    77
    78
    79
    80
    81
    def get_default_namespace():
        """Return the Global namespace."""
        obj, _ = Namespace.objects.get_or_create(
            name="Global", defaults={"description": "Default Global namespace. Created by Nautobot."}
        )
        return obj

## `nautobot.apps.models.get_default_namespace_pk()`

Return the PK of the Global namespace for use in default value for foreign keys.

Source code in `nautobot/ipam/models.py`

    84
    85
    86
    def get_default_namespace_pk():
        """Return the PK of the Global namespace for use in default value for foreign keys."""
        return get_default_namespace().pk

## `nautobot.apps.models.is_taggable(obj)`

Return True if the instance can have Tags assigned to it; False otherwise.

Source code in `nautobot/core/models/utils.py`

    37
    38
    39
    40
    41
    42
    43
    def is_taggable(obj):
        """
        Return True if the instance can have Tags assigned to it; False otherwise.
        """
        from nautobot.core.models.managers import TagsManager

        return hasattr(obj, "tags") and isinstance(obj.tags, TagsManager)

## `nautobot.apps.models.naturalize(value, max_length, integer_places=8)`

Take an alphanumeric string and prepend all integers to `integer_places` places to ensure the strings are ordered naturally. For example:

    location9router21
    location10router4
    location10router19

becomes

location00000009router00000021 location00000010router00000004 location00000010router00000019

:param value: The value to be naturalized :param max_length: The maximum length of the returned string. Characters beyond this length will be stripped. :param integer_places: The number of places to which each integer will be expanded. (Default: 8)

Source code in `nautobot/core/models/ordering.py`

    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    def naturalize(value, max_length, integer_places=8):
        """
        Take an alphanumeric string and prepend all integers to `integer_places` places to ensure the strings
        are ordered naturally. For example:

            location9router21
            location10router4
            location10router19

        becomes:

            location00000009router00000021
            location00000010router00000004
            location00000010router00000019

        :param value: The value to be naturalized
        :param max_length: The maximum length of the returned string. Characters beyond this length will be stripped.
        :param integer_places: The number of places to which each integer will be expanded. (Default: 8)
        """
        if not value:
            return value
        output = []
        for segment in re.split(r"(\d+)", value):
            if segment.isdigit():
                output.append(segment.rjust(integer_places, "0"))
            elif segment:
                output.append(segment)
        ret = "".join(output)

        return ret[:max_length]

## `nautobot.apps.models.naturalize_interface(value, max_length)`

Similar in nature to naturalize(), but takes into account a particular naming format adapted from the old InterfaceManager.

:param value: The value to be naturalized :param max_length: The maximum length of the returned string. Characters beyond this length will be stripped.

Source code in `nautobot/core/models/ordering.py`

    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
    def naturalize_interface(value, max_length):
        """
        Similar in nature to naturalize(), but takes into account a particular naming format adapted from the old
        InterfaceManager.

        :param value: The value to be naturalized
        :param max_length: The maximum length of the returned string. Characters beyond this length will be stripped.
        """
        output = ""
        match = re.search(INTERFACE_NAME_REGEX, value)
        if match is None:
            return value

        # First, we order by slot/position, padding each to four digits. If a field is not present,
        # set it to 9999 to ensure it is ordered last.
        for part_name in ("slot", "subslot", "position", "subposition"):
            part = match.group(part_name)
            if part is not None:
                output += part.rjust(4, "0")
            else:
                output += "9999"

        # Append the type, if any.
        if match.group("type") is not None:
            output += match.group("type")

        # Append any remaining fields, left-padding to six digits each.
        for part_name in ("id", "channel", "vc"):
            part = match.group(part_name)
            if part is not None:
                output += part.rjust(6, "0")
            else:
                output += "......"

        # Finally, naturalize any remaining text and append it
        if match.group("remainder") is not None and len(output) < max_length:
            remainder = naturalize(match.group("remainder"), max_length - len(output))
            output += remainder

        return output[:max_length]

## `nautobot.apps.models.pretty_print_query(query)`

Given a `Q` object, display it in a more human-readable format.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`query` | `Q` | Query to display. | required

Returns:

Type | Description
--- | ---
`str` | Pretty-printed query logic

Example

> > > print(pretty_print_query(Q)) ( location__name='Campus-01' OR location__name='Campus-02' OR ( location__name='Room-01' AND status__name='Active' ) OR ( location__name='Building-01' AND ( NOT (location__name='Building-01' AND status__name='Decommissioning') ) ) )

Source code in `nautobot/core/models/utils.py`

    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
    def pretty_print_query(query):
        """
        Given a `Q` object, display it in a more human-readable format.

        Args:
            query (Q): Query to display.

        Returns:
            (str): Pretty-printed query logic

        Example:
            >>> print(pretty_print_query(Q))
            (
              location__name='Campus-01' OR location__name='Campus-02' OR (
                location__name='Room-01' AND status__name='Active'
              ) OR (
                location__name='Building-01' AND (
                  NOT (location__name='Building-01' AND status__name='Decommissioning')
                )
              )
            )
        """

        def pretty_str(self, node=None, depth=0):
            """Improvement to default `Node.__str__` with a more human-readable style."""
            template = f"(\n{'  ' * (depth + 1)}"
            if self.negated:
                template += "NOT (%s)"
            else:
                template += "%s"
            template += f"\n{'  ' * depth})"
            children = []

            # If we don't have a node, we are the node!
            if node is None:
                node = self

            # Iterate over children. They will be either a Q object (a Node subclass) or a 2-tuple.
            for child in node.children:
                # Trust that we can stringify the child if it is a Node instance.
                if isinstance(child, Node):
                    children.append(pretty_str(child, depth=depth + 1))
                # If a 2-tuple, stringify to key=value
                else:
                    key, value = child
                    children.append(f"{key}={value!r}")

            return template % (f" {self.connector} ".join(children))

        # Use pretty_str() as the string generator vs. just stringify the `Q` object.
        return pretty_str(query)

## `nautobot.apps.models.serialize_object(obj, extra=None, exclude=None)`

Return a generic JSON representation of an object using Django's built-in serializer. (This is used for things like change logging, not the REST API.) Optionally include a dictionary to supplement the object data. A list of keys can be provided to exclude them from the returned dictionary. Private fields (prefaced with an underscore) are implicitly excluded.

Source code in `nautobot/core/models/utils.py`

     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    def serialize_object(obj, extra=None, exclude=None):
        """
        Return a generic JSON representation of an object using Django's built-in serializer. (This is used for things like
        change logging, not the REST API.) Optionally include a dictionary to supplement the object data. A list of keys
        can be provided to exclude them from the returned dictionary. Private fields (prefaced with an underscore) are
        implicitly excluded.
        """
        json_str = serialize("json", [obj])
        data = json.loads(json_str)[0]["fields"]

        # Include custom_field_data as "custom_fields"
        if hasattr(obj, "_custom_field_data"):
            data["custom_fields"] = data.pop("_custom_field_data")

        # Include any tags. Check for tags cached on the instance; fall back to using the manager.
        if is_taggable(obj):
            tags = getattr(obj, "_tags", []) or obj.tags.all()
            data["tags"] = [tag.name for tag in tags]

        # Append any extra data
        if extra is not None:
            data.update(extra)

        # Copy keys to list to avoid 'dictionary changed size during iteration' exception
        for key in list(data):
            # Private fields shouldn't be logged in the object change
            if isinstance(key, str) and key.startswith("_"):
                data.pop(key)

            # Explicitly excluded keys
            if isinstance(exclude, (list, tuple)) and key in exclude:
                data.pop(key)

        return data

## `nautobot.apps.models.serialize_object_v2(obj)`

Return a JSON serialized representation of an object using obj's serializer.

Source code in `nautobot/core/models/utils.py`

    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    def serialize_object_v2(obj):
        """
        Return a JSON serialized representation of an object using obj's serializer.
        """
        from nautobot.core.api.exceptions import SerializerNotFound
        from nautobot.core.api.utils import get_serializer_for_model

        # Try serializing obj(model instance) using its API Serializer
        try:
            serializer_class = get_serializer_for_model(obj.__class__)
            data = serializer_class(obj, context={"request": None, "depth": 1}).data
        except SerializerNotFound:
            # Fall back to generic JSON representation of obj
            data = serialize_object(obj)

        return data

## `nautobot.apps.models.slugify_dashes_to_underscores(content)`

Custom slugify_function - use underscores instead of dashes; resulting slug can be used as a variable name, as well as a graphql safe string. Note: If content starts with a non graphql-safe character, e.g. a digit This method will prepend an "a" to content to make it graphql-safe e.g: 123 main st -> a123_main_st

Source code in `nautobot/core/models/fields.py`

    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    def slugify_dashes_to_underscores(content):
        """
        Custom slugify_function - use underscores instead of dashes; resulting slug can be used as a variable name,
        as well as a graphql safe string.
        Note: If content starts with a non graphql-safe character, e.g. a digit
        This method will prepend an "a" to content to make it graphql-safe
        e.g:
            123 main st -> a123_main_st
        """
        graphql_safe_pattern = re.compile("[_A-Za-z]")
        # If the first letter of the slug is not GraphQL safe.
        # We append "a" to it.
        if graphql_safe_pattern.fullmatch(content[0]) is None:
            content = "a" + content
        return slugify(content).replace("-", "_")

## `nautobot.apps.models.slugify_dots_to_dashes(content)`

Custom slugify_function - convert '.' to '-' instead of removing dots outright.

Source code in `nautobot/core/models/fields.py`

    61
    62
    63
    def slugify_dots_to_dashes(content):
        """Custom slugify_function - convert '.' to '-' instead of removing dots outright."""
        return slugify(content.replace(".", "-"))