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

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

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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
def slugify_dots_to_dashes(content):
    """Custom slugify_function - convert '.' to '-' instead of removing dots outright."""
    return slugify(content.replace(".", "-"))