Skip to content

nautobot.apps.views

Utilities for apps to implement UI views.

nautobot.apps.views.AdminRequiredMixin

Bases: AccessMixin

Allows access only to admin users.

Source code in nautobot/core/views/mixins.py
class AdminRequiredMixin(AccessMixin):
    """
    Allows access only to admin users.
    """

    def has_permission(self):
        return bool(
            self.request.user
            and self.request.user.is_active
            and (self.request.user.is_staff or self.request.user.is_superuser)
        )

    def dispatch(self, request, *args, **kwargs):
        if not self.has_permission():
            return self.handle_no_permission()

        return super().dispatch(request, *args, **kwargs)

nautobot.apps.views.BulkComponentCreateView

Bases: GetReturnURLMixin, ObjectPermissionRequiredMixin, View

Add one or more components (e.g. interfaces, console ports, etc.) to a set of Devices or VirtualMachines.

Source code in nautobot/core/views/generic.py
class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
    """
    Add one or more components (e.g. interfaces, console ports, etc.) to a set of Devices or VirtualMachines.
    """

    parent_model = None
    parent_field = None
    form = None
    queryset = None
    model_form = None
    filterset = None
    table = None
    template_name = "generic/object_bulk_add_component.html"

    def get_required_permission(self):
        return f"dcim.add_{self.queryset.model._meta.model_name}"

    def post(self, request):
        logger = logging.getLogger(__name__ + ".BulkComponentCreateView")
        parent_model_name = self.parent_model._meta.verbose_name_plural
        model_name = self.queryset.model._meta.verbose_name_plural
        model = self.queryset.model

        # Are we editing *all* objects in the queryset or just a selected subset?
        if request.POST.get("_all") and self.filterset is not None:
            pk_list = [obj.pk for obj in self.filterset(request.GET, self.parent_model.objects.only("pk")).qs]
        else:
            pk_list = request.POST.getlist("pk")

        selected_objects = self.parent_model.objects.filter(pk__in=pk_list)
        if not selected_objects:
            messages.warning(
                request,
                f"No {self.parent_model._meta.verbose_name_plural} were selected.",
            )
            return redirect(self.get_return_url(request))
        table = self.table(selected_objects)

        if "_create" in request.POST:
            form = self.form(model, request.POST)

            if form.is_valid():
                logger.debug("Form validation was successful")

                new_components = []
                data = deepcopy(form.cleaned_data)

                try:
                    with transaction.atomic():
                        for obj in data["pk"]:
                            names = data["name_pattern"]
                            labels = data["label_pattern"] if "label_pattern" in data else None
                            for i, name in enumerate(names):
                                label = labels[i] if labels else None

                                component_data = {
                                    self.parent_field: obj.pk,
                                    "name": name,
                                    "label": label,
                                }
                                component_data.update(data)
                                component_form = self.model_form(component_data)
                                if component_form.is_valid():
                                    instance = component_form.save()
                                    logger.debug(f"Created {instance} on {instance.parent}")
                                    new_components.append(instance)
                                else:
                                    for (
                                        field,
                                        errors,
                                    ) in component_form.errors.as_data().items():
                                        for e in errors:
                                            err_str = ", ".join(e)
                                            form.add_error(
                                                field,
                                                f"{obj} {name}: {err_str}",
                                            )

                        # Enforce object-level permissions
                        if self.queryset.filter(pk__in=[obj.pk for obj in new_components]).count() != len(
                            new_components
                        ):
                            raise ObjectDoesNotExist

                except IntegrityError:
                    pass

                except ObjectDoesNotExist:
                    msg = "Component creation failed due to object-level permissions violation"
                    logger.debug(msg)
                    form.add_error(None, msg)

                if not form.errors:
                    msg = f"Added {len(new_components)} {model_name} to {len(form.cleaned_data['pk'])} {parent_model_name}."
                    logger.info(msg)
                    messages.success(request, msg)

                    return redirect(self.get_return_url(request))

            else:
                logger.debug("Form validation failed")

        else:
            form = self.form(model, initial={"pk": pk_list})

        return render(
            request,
            self.template_name,
            {
                "form": form,
                "parent_model_name": parent_model_name,
                "model_name": model_name,
                "table": table,
                "return_url": self.get_return_url(request),
            },
        )

nautobot.apps.views.BulkCreateView

Bases: GetReturnURLMixin, ObjectPermissionRequiredMixin, View

Create new objects in bulk.

queryset: Base queryset for the objects being created form: Form class which provides the pattern field model_form: The ModelForm used to create individual objects pattern_target: Name of the field to be evaluated as a pattern (if any) template_name: The name of the template

Source code in nautobot/core/views/generic.py
class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
    """
    Create new objects in bulk.

    queryset: Base queryset for the objects being created
    form: Form class which provides the `pattern` field
    model_form: The ModelForm used to create individual objects
    pattern_target: Name of the field to be evaluated as a pattern (if any)
    template_name: The name of the template
    """

    queryset = None
    form = None
    model_form = None
    pattern_target = ""
    template_name = None

    def get_required_permission(self):
        return get_permission_for_model(self.queryset.model, "add")

    def get(self, request):
        # Set initial values for visible form fields from query args
        initial = {}
        for field in getattr(self.model_form._meta, "fields", []):
            if request.GET.get(field):
                initial[field] = request.GET[field]

        form = self.form()
        model_form = self.model_form(initial=initial)

        return render(
            request,
            self.template_name,
            {
                "obj_type": self.model_form._meta.model._meta.verbose_name,
                "form": form,
                "model_form": model_form,
                "return_url": self.get_return_url(request),
            },
        )

    def post(self, request):
        logger = logging.getLogger(__name__ + ".BulkCreateView")
        model = self.queryset.model
        form = self.form(request.POST)
        model_form = self.model_form(request.POST)

        if form.is_valid():
            logger.debug("Form validation was successful")
            pattern = form.cleaned_data["pattern"]
            new_objs = []

            try:
                with transaction.atomic():
                    # Create objects from the expanded. Abort the transaction on the first validation error.
                    for value in pattern:
                        # Reinstantiate the model form each time to avoid overwriting the same instance. Use a mutable
                        # copy of the POST QueryDict so that we can update the target field value.
                        model_form = self.model_form(request.POST.copy())
                        model_form.data[self.pattern_target] = value

                        # Validate each new object independently.
                        if model_form.is_valid():
                            obj = model_form.save()
                            logger.debug(f"Created {obj} (PK: {obj.pk})")
                            new_objs.append(obj)
                        else:
                            # Copy any errors on the pattern target field to the pattern form.
                            errors = model_form.errors.as_data()
                            if errors.get(self.pattern_target):
                                form.add_error("pattern", errors[self.pattern_target])
                            # Raise an IntegrityError to break the for loop and abort the transaction.
                            raise IntegrityError()

                    # Enforce object-level permissions
                    if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
                        raise ObjectDoesNotExist

                    # If we make it to this point, validation has succeeded on all new objects.
                    msg = f"Added {len(new_objs)} {model._meta.verbose_name_plural}"
                    logger.info(msg)
                    messages.success(request, msg)

                    if "_addanother" in request.POST:
                        return redirect(request.path)
                    return redirect(self.get_return_url(request))

            except IntegrityError:
                pass

            except ObjectDoesNotExist:
                msg = "Object creation failed due to object-level permissions violation"
                logger.debug(msg)
                form.add_error(None, msg)

        else:
            logger.debug("Form validation failed")

        return render(
            request,
            self.template_name,
            {
                "form": form,
                "model_form": model_form,
                "obj_type": model._meta.verbose_name,
                "return_url": self.get_return_url(request),
            },
        )

nautobot.apps.views.BulkDeleteView

Bases: GetReturnURLMixin, ObjectPermissionRequiredMixin, View

Delete objects in bulk.

queryset: Custom queryset to use when retrieving objects (e.g. to select related objects) filter: FilterSet to apply when deleting by QuerySet table: The table used to display devices being deleted form: The form class used to delete objects in bulk template_name: The name of the template

Source code in nautobot/core/views/generic.py
class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
    """
    Delete objects in bulk.

    queryset: Custom queryset to use when retrieving objects (e.g. to select related objects)
    filter: FilterSet to apply when deleting by QuerySet
    table: The table used to display devices being deleted
    form: The form class used to delete objects in bulk
    template_name: The name of the template
    """

    queryset = None
    filterset = None
    table = None
    form = None
    template_name = "generic/object_bulk_delete.html"

    def get_required_permission(self):
        return get_permission_for_model(self.queryset.model, "delete")

    def get(self, request):
        return redirect(self.get_return_url(request))

    def post(self, request, **kwargs):
        logger = logging.getLogger(__name__ + ".BulkDeleteView")
        model = self.queryset.model

        # Are we deleting *all* objects in the queryset or just a selected subset?
        if request.POST.get("_all"):
            if self.filterset is not None:
                pk_list = list(self.filterset(request.GET, model.objects.only("pk")).qs.values_list("pk", flat=True))
            else:
                pk_list = list(model.objects.all().values_list("pk", flat=True))
        else:
            pk_list = request.POST.getlist("pk")

        form_cls = self.get_form()

        if "_confirm" in request.POST:
            form = form_cls(request.POST)
            if form.is_valid():
                logger.debug("Form validation was successful")

                # Delete objects
                queryset = self.queryset.filter(pk__in=pk_list)

                self.perform_pre_delete(request, queryset)
                try:
                    _, deleted_info = queryset.delete()
                    deleted_count = deleted_info[model._meta.label]
                except ProtectedError as e:
                    logger.info("Caught ProtectedError while attempting to delete objects")
                    handle_protectederror(queryset, request, e)
                    return redirect(self.get_return_url(request))

                msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
                logger.info(msg)
                messages.success(request, msg)
                return redirect(self.get_return_url(request))

            else:
                logger.debug("Form validation failed")

        else:
            form = form_cls(
                initial={
                    "pk": pk_list,
                    "return_url": self.get_return_url(request),
                }
            )

        # Retrieve objects being deleted
        table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False)
        if not table.rows:
            messages.warning(
                request,
                f"No {model._meta.verbose_name_plural} were selected for deletion.",
            )
            return redirect(self.get_return_url(request))
        # Hide actions column if present
        if "actions" in table.columns:
            table.columns.hide("actions")

        context = {
            "form": form,
            "obj_type_plural": model._meta.verbose_name_plural,
            "table": table,
            "return_url": self.get_return_url(request),
        }
        context.update(self.extra_context())
        return render(request, self.template_name, context)

    def perform_pre_delete(self, request, queryset):
        pass

    def extra_context(self):
        return {}

    def get_form(self):
        """
        Provide a standard bulk delete form if none has been specified for the view
        """

        class BulkDeleteForm(ConfirmationForm):
            pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput)

        if self.form:
            return self.form

        return BulkDeleteForm

get_form()

Provide a standard bulk delete form if none has been specified for the view

Source code in nautobot/core/views/generic.py
def get_form(self):
    """
    Provide a standard bulk delete form if none has been specified for the view
    """

    class BulkDeleteForm(ConfirmationForm):
        pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput)

    if self.form:
        return self.form

    return BulkDeleteForm

nautobot.apps.views.BulkEditView

Bases: GetReturnURLMixin, ObjectPermissionRequiredMixin, View

Edit objects in bulk.

queryset: Custom queryset to use when retrieving objects (e.g. to select related objects) filter: FilterSet to apply when deleting by QuerySet table: The table used to display devices being edited form: The form class used to edit objects in bulk template_name: The name of the template

Source code in nautobot/core/views/generic.py
class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
    """
    Edit objects in bulk.

    queryset: Custom queryset to use when retrieving objects (e.g. to select related objects)
    filter: FilterSet to apply when deleting by QuerySet
    table: The table used to display devices being edited
    form: The form class used to edit objects in bulk
    template_name: The name of the template
    """

    queryset = None
    filterset = None
    table = None
    form = None
    template_name = "generic/object_bulk_edit.html"

    def get_required_permission(self):
        return get_permission_for_model(self.queryset.model, "change")

    def get(self, request):
        return redirect(self.get_return_url(request))

    def alter_obj(self, obj, request, url_args, url_kwargs):
        # Allow views to add extra info to an object before it is processed.
        # For example, a parent object can be defined given some parameter from the request URL.
        return obj

    def post(self, request, **kwargs):
        logger = logging.getLogger(__name__ + ".BulkEditView")
        model = self.queryset.model

        # If we are editing *all* objects in the queryset, replace the PK list with all matched objects.
        if request.POST.get("_all"):
            if self.filterset is not None:
                pk_list = list(self.filterset(request.GET, model.objects.only("pk")).qs.values_list("pk", flat=True))
            else:
                pk_list = list(model.objects.all().values_list("pk", flat=True))
        else:
            pk_list = request.POST.getlist("pk")

        if "_apply" in request.POST:
            form = self.form(model, request.POST)
            restrict_form_fields(form, request.user)

            if form.is_valid():
                logger.debug("Form validation was successful")
                form_custom_fields = getattr(form, "custom_fields", [])
                form_relationships = getattr(form, "relationships", [])
                standard_fields = [
                    field
                    for field in form.fields
                    if field not in form_custom_fields + form_relationships + ["pk"] + ["object_note"]
                ]
                nullified_fields = request.POST.getlist("_nullify")

                try:
                    with transaction.atomic():
                        updated_objects = []
                        for obj in self.queryset.filter(pk__in=form.cleaned_data["pk"]):
                            obj = self.alter_obj(obj, request, [], kwargs)

                            # Update standard fields. If a field is listed in _nullify, delete its value.
                            for name in standard_fields:
                                try:
                                    model_field = model._meta.get_field(name)
                                except FieldDoesNotExist:
                                    # This form field is used to modify a field rather than set its value directly
                                    model_field = None

                                # Handle nullification
                                if name in form.nullable_fields and name in nullified_fields:
                                    if isinstance(model_field, ManyToManyField):
                                        getattr(obj, name).set([])
                                    else:
                                        setattr(obj, name, None if model_field is not None and model_field.null else "")

                                # ManyToManyFields
                                elif isinstance(model_field, ManyToManyField):
                                    if form.cleaned_data[name]:
                                        getattr(obj, name).set(form.cleaned_data[name])
                                # Normal fields
                                elif form.cleaned_data[name] not in (None, ""):
                                    setattr(obj, name, form.cleaned_data[name])

                            # Update custom fields
                            for field_name in form_custom_fields:
                                if field_name in form.nullable_fields and field_name in nullified_fields:
                                    obj.cf[remove_prefix_from_cf_key(field_name)] = None
                                elif form.cleaned_data.get(field_name) not in (None, "", []):
                                    obj.cf[remove_prefix_from_cf_key(field_name)] = form.cleaned_data[field_name]

                            obj.full_clean()
                            obj.save()
                            updated_objects.append(obj)
                            logger.debug(f"Saved {obj} (PK: {obj.pk})")

                            # Add/remove tags
                            if form.cleaned_data.get("add_tags", None):
                                obj.tags.add(*form.cleaned_data["add_tags"])
                            if form.cleaned_data.get("remove_tags", None):
                                obj.tags.remove(*form.cleaned_data["remove_tags"])

                            if hasattr(form, "save_relationships") and callable(form.save_relationships):
                                # Add/remove relationship associations
                                form.save_relationships(instance=obj, nullified_fields=nullified_fields)

                            if hasattr(form, "save_note") and callable(form.save_note):
                                form.save_note(instance=obj, user=request.user)

                        # Enforce object-level permissions
                        if self.queryset.filter(pk__in=[obj.pk for obj in updated_objects]).count() != len(
                            updated_objects
                        ):
                            raise ObjectDoesNotExist

                    if updated_objects:
                        msg = f"Updated {len(updated_objects)} {model._meta.verbose_name_plural}"
                        logger.info(msg)
                        messages.success(self.request, msg)

                    return redirect(self.get_return_url(request))

                except ValidationError as e:
                    messages.error(self.request, f"{obj} failed validation: {e}")

                except ObjectDoesNotExist:
                    msg = "Object update failed due to object-level permissions violation"
                    logger.debug(msg)
                    form.add_error(None, msg)

            else:
                logger.debug("Form validation failed")

        else:
            # Include the PK list as initial data for the form
            initial_data = {"pk": pk_list}

            # Check for other contextual data needed for the form. We avoid passing all of request.GET because the
            # filter values will conflict with the bulk edit form fields.
            # TODO: Find a better way to accomplish this
            if "device" in request.GET:
                initial_data["device"] = request.GET.get("device")
            elif "device_type" in request.GET:
                initial_data["device_type"] = request.GET.get("device_type")

            form = self.form(model, initial=initial_data)
            restrict_form_fields(form, request.user)

        # Retrieve objects being edited
        table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False)
        if not table.rows:
            messages.warning(request, f"No {model._meta.verbose_name_plural} were selected.")
            return redirect(self.get_return_url(request))
        # Hide actions column if present
        if "actions" in table.columns:
            table.columns.hide("actions")

        context = {
            "form": form,
            "table": table,
            "obj_type_plural": model._meta.verbose_name_plural,
            "return_url": self.get_return_url(request),
        }
        context.update(self.extra_context())
        return render(request, self.template_name, context)

    def extra_context(self):
        return {}

nautobot.apps.views.BulkImportView

Bases: GetReturnURLMixin, ObjectPermissionRequiredMixin, View

Import objects in bulk (CSV format).

queryset: Base queryset for the model table: The django-tables2 Table used to render the list of imported objects template_name: The name of the template

Source code in nautobot/core/views/generic.py
class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
    """
    Import objects in bulk (CSV format).

    queryset: Base queryset for the model
    table: The django-tables2 Table used to render the list of imported objects
    template_name: The name of the template
    """

    queryset = None
    table = None
    template_name = "generic/object_bulk_import.html"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.serializer_class = get_serializer_for_model(self.queryset.model)
        self.fields = get_csv_form_fields_from_serializer_class(self.serializer_class)
        self.required_field_names = [
            field["name"]
            for field in get_csv_form_fields_from_serializer_class(self.serializer_class)
            if field["required"]
        ]

    def _import_form(self, *args, **kwargs):
        class CSVImportForm(BootstrapMixin, Form):
            csv_data = CSVDataField(required_field_names=self.required_field_names)
            csv_file = CSVFileField()

        return CSVImportForm(*args, **kwargs)

    def get_required_permission(self):
        return get_permission_for_model(self.queryset.model, "add")

    def get(self, request):
        return render(
            request,
            self.template_name,
            {
                "form": self._import_form(),
                "fields": self.fields,
                "obj_type": self.queryset.model._meta.verbose_name,
                "return_url": self.get_return_url(request),
                "active_tab": "csv-data",
            },
        )

    def post(self, request):
        logger = logging.getLogger(__name__ + ".BulkImportView")
        new_objs = []
        form = self._import_form(request.POST, request.FILES)

        if form.is_valid():
            logger.debug("Form validation was successful")

            try:
                # Iterate through CSV data and bind each row to a new model form instance.
                with transaction.atomic():
                    new_objs = import_csv_helper(request=request, form=form, serializer_class=self.serializer_class)

                    # Enforce object-level permissions
                    if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
                        raise ObjectDoesNotExist

                # Compile a table containing the imported objects
                obj_table = self.table(new_objs)

                if new_objs:
                    msg = f"Imported {len(new_objs)} {new_objs[0]._meta.verbose_name_plural}"
                    logger.info(msg)
                    messages.success(request, msg)

                    return render(
                        request,
                        "import_success.html",
                        {
                            "table": obj_table,
                            "return_url": self.get_return_url(request),
                        },
                    )

            except ValidationError:
                pass

            except ObjectDoesNotExist:
                msg = "Object import failed due to object-level permissions violation"
                logger.debug(msg)
                form.add_error(None, msg)

        else:
            logger.debug("Form validation failed")

        return render(
            request,
            self.template_name,
            {
                "form": form,
                "fields": self.fields,
                "obj_type": self.queryset.model._meta.verbose_name,
                "return_url": self.get_return_url(request),
                "active_tab": "csv-file" if form.has_error("csv_file") else "csv-data",
            },
        )

nautobot.apps.views.BulkRenameView

Bases: GetReturnURLMixin, ObjectPermissionRequiredMixin, View

An extendable view for renaming objects in bulk.

Source code in nautobot/core/views/generic.py
class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
    """
    An extendable view for renaming objects in bulk.
    """

    queryset = None
    template_name = "generic/object_bulk_rename.html"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Create a new Form class from BulkRenameForm
        class _Form(BulkRenameForm):
            pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput())

        self.form = _Form

    def get_required_permission(self):
        return get_permission_for_model(self.queryset.model, "change")

    def post(self, request):
        logger = logging.getLogger(__name__ + ".BulkRenameView")
        query_pks = request.POST.getlist("pk")
        selected_objects = self.queryset.filter(pk__in=query_pks) if query_pks else None

        # selected_objects would return False; if no query_pks or invalid query_pks
        if not selected_objects:
            messages.warning(request, f"No valid {self.queryset.model._meta.verbose_name_plural} were selected.")
            return redirect(self.get_return_url(request))

        if "_preview" in request.POST or "_apply" in request.POST:
            form = self.form(request.POST, initial={"pk": query_pks})
            if form.is_valid():
                try:
                    with transaction.atomic():
                        renamed_pks = []
                        for obj in selected_objects:
                            find = form.cleaned_data["find"]
                            replace = form.cleaned_data["replace"]
                            if form.cleaned_data["use_regex"]:
                                try:
                                    obj.new_name = re.sub(find, replace, obj.name)
                                # Catch regex group reference errors
                                except re.error:
                                    obj.new_name = obj.name
                            else:
                                obj.new_name = obj.name.replace(find, replace)
                            renamed_pks.append(obj.pk)

                        if "_apply" in request.POST:
                            for obj in selected_objects:
                                obj.name = obj.new_name
                                obj.save()

                            # Enforce constrained permissions
                            if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects):
                                raise ObjectDoesNotExist

                            messages.success(
                                request,
                                f"Renamed {len(selected_objects)} {self.queryset.model._meta.verbose_name_plural}",
                            )
                            return redirect(self.get_return_url(request))

                except ObjectDoesNotExist:
                    msg = "Object update failed due to object-level permissions violation"
                    logger.debug(msg)
                    form.add_error(None, msg)

        else:
            form = self.form(initial={"pk": query_pks})

        return render(
            request,
            self.template_name,
            {
                "form": form,
                "obj_type_plural": self.queryset.model._meta.verbose_name_plural,
                "selected_objects": selected_objects,
                "return_url": self.get_return_url(request),
                "parent_name": self.get_selected_objects_parents_name(selected_objects),
            },
        )

    def get_selected_objects_parents_name(self, selected_objects):
        """
        Return selected_objects parent name.

        This method is intended to be overridden by child classes to return the parent name of the selected objects.

        Args:
            selected_objects (list[BaseModel]): The objects being renamed

        Returns:
            (str): The parent name of the selected objects
        """

        return ""

get_selected_objects_parents_name(selected_objects)

Return selected_objects parent name.

This method is intended to be overridden by child classes to return the parent name of the selected objects.

Parameters:

Name Type Description Default
selected_objects list[BaseModel]

The objects being renamed

required

Returns:

Type Description
str

The parent name of the selected objects

Source code in nautobot/core/views/generic.py
def get_selected_objects_parents_name(self, selected_objects):
    """
    Return selected_objects parent name.

    This method is intended to be overridden by child classes to return the parent name of the selected objects.

    Args:
        selected_objects (list[BaseModel]): The objects being renamed

    Returns:
        (str): The parent name of the selected objects
    """

    return ""

nautobot.apps.views.ComponentCreateView

Bases: GetReturnURLMixin, ObjectPermissionRequiredMixin, View

Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine.

Source code in nautobot/core/views/generic.py
class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
    """
    Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine.
    """

    queryset = None
    form = None
    model_form = None
    template_name = None

    def get_required_permission(self):
        return get_permission_for_model(self.queryset.model, "add")

    def get(self, request):
        form = self.form(initial=request.GET)
        model_form = self.model_form(request.GET)

        return render(
            request,
            self.template_name,
            {
                "component_type": self.queryset.model._meta.verbose_name,
                "model_form": model_form,
                "form": form,
                "return_url": self.get_return_url(request),
            },
        )

    def post(self, request):
        logger = logging.getLogger(__name__ + ".ComponentCreateView")
        form = self.form(request.POST, initial=request.GET)
        model_form = self.model_form(request.POST)

        if form.is_valid():
            new_components = []
            data = deepcopy(request.POST)

            names = form.cleaned_data["name_pattern"]
            labels = form.cleaned_data.get("label_pattern")
            for i, name in enumerate(names):
                label = labels[i] if labels else None
                # Initialize the individual component form
                data["name"] = name
                data["label"] = label
                if hasattr(form, "get_iterative_data"):
                    data.update(form.get_iterative_data(i))
                component_form = self.model_form(data)

                if component_form.is_valid():
                    new_components.append(component_form)
                else:
                    for field, errors in component_form.errors.as_data().items():
                        # Assign errors on the child form's name/label field to name_pattern/label_pattern on the parent form
                        if field == "name":
                            field = "name_pattern"
                        elif field == "label":
                            field = "label_pattern"
                        for e in errors:
                            err_str = ", ".join(e)
                            form.add_error(field, f"{name}: {err_str}")

            if not form.errors:
                try:
                    with transaction.atomic():
                        # Create the new components
                        new_objs = []
                        for component_form in new_components:
                            obj = component_form.save()
                            new_objs.append(obj)

                        # Enforce object-level permissions
                        if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
                            raise ObjectDoesNotExist

                    messages.success(
                        request,
                        f"Added {len(new_components)} {self.queryset.model._meta.verbose_name_plural}",
                    )
                    if "_addanother" in request.POST:
                        return redirect(request.get_full_path())
                    else:
                        return redirect(self.get_return_url(request))

                except ObjectDoesNotExist:
                    msg = "Component creation failed due to object-level permissions violation"
                    logger.debug(msg)
                    form.add_error(None, msg)

        return render(
            request,
            self.template_name,
            {
                "component_type": self.queryset.model._meta.verbose_name,
                "form": form,
                "model_form": model_form,
                "return_url": self.get_return_url(request),
            },
        )

nautobot.apps.views.ContentTypePermissionRequiredMixin

Bases: AccessMixin

Similar to Django's built-in PermissionRequiredMixin, but extended to check model-level permission assignments. This is related to ObjectPermissionRequiredMixin, except that is does not enforce object-level permissions, and fits within Nautobot's custom permission enforcement system.

An optional iterable of statically declared permissions to evaluate in addition to those

derived from the object type

Source code in nautobot/core/views/mixins.py
class ContentTypePermissionRequiredMixin(AccessMixin):
    """
    Similar to Django's built-in PermissionRequiredMixin, but extended to check model-level permission assignments.
    This is related to ObjectPermissionRequiredMixin, except that is does not enforce object-level permissions,
    and fits within Nautobot's custom permission enforcement system.

    additional_permissions: An optional iterable of statically declared permissions to evaluate in addition to those
                            derived from the object type
    """

    additional_permissions = []

    def get_required_permission(self):
        """
        Return the specific permission necessary to perform the requested action on an object.
        """
        raise NotImplementedError(f"{self.__class__.__name__} must implement get_required_permission()")

    def has_permission(self):
        user = self.request.user
        permission_required = self.get_required_permission()

        # Check that the user has been granted the required permission(s).
        if user.has_perms((permission_required, *self.additional_permissions)):
            return True

        return False

    def dispatch(self, request, *args, **kwargs):
        if not self.has_permission():
            return self.handle_no_permission()

        return super().dispatch(request, *args, **kwargs)

get_required_permission()

Return the specific permission necessary to perform the requested action on an object.

Source code in nautobot/core/views/mixins.py
def get_required_permission(self):
    """
    Return the specific permission necessary to perform the requested action on an object.
    """
    raise NotImplementedError(f"{self.__class__.__name__} must implement get_required_permission()")

nautobot.apps.views.GetReturnURLMixin

Provides logic for determining where a user should be redirected after processing a form.

Source code in nautobot/core/views/mixins.py
class GetReturnURLMixin:
    """
    Provides logic for determining where a user should be redirected after processing a form.
    """

    default_return_url = None

    def get_return_url(self, request, obj=None, default_return_url=None):
        # First, see if `return_url` was specified as a query parameter or form data. Use this URL only if it's
        # considered safe.
        query_param = request.GET.get("return_url") or request.POST.get("return_url")
        if url_has_allowed_host_and_scheme(url=query_param, allowed_hosts=request.get_host()):
            return iri_to_uri(query_param)

        # Next, check if the object being modified (if any) has an absolute URL.
        # Note that the use of both `obj.present_in_database` and `obj.pk` is correct here because this conditional
        # handles all three of the create, update, and delete operations. When Django deletes an instance
        # from the DB, it sets the instance's PK field to None, regardless of the use of a UUID.
        try:
            if obj is not None and obj.present_in_database and obj.pk:
                return obj.get_absolute_url()
        except AttributeError:
            # Model has no get_absolute_url() method or no reverse match
            pass

        if default_return_url is not None:
            return reverse(default_return_url)

        # Fall back to the default URL (if specified) for the view.
        if self.default_return_url is not None:
            return reverse(self.default_return_url)

        # Attempt to dynamically resolve the list view for the object
        if hasattr(self, "queryset"):
            try:
                return reverse(lookup.get_route_for_model(self.queryset.model, "list"))
            except NoReverseMatch:
                pass

        # If all else fails, return home. Ideally this should never happen.
        return reverse("home")

nautobot.apps.views.NautobotHTMLRenderer

Bases: renderers.BrowsableAPIRenderer

Inherited from BrowsableAPIRenderer to do most of the heavy lifting for getting the context needed for templates and template rendering.

Source code in nautobot/core/views/renderers.py
class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
    """
    Inherited from BrowsableAPIRenderer to do most of the heavy lifting for getting the context needed for templates and template rendering.
    """

    # Log error messages within NautobotHTMLRenderer
    logger = logging.getLogger(__name__)

    def get_dynamic_filter_form(self, view, request, *args, filterset_class=None, **kwargs):
        """
        Helper function to obtain the filter_form_class,
        and then initialize and return the filter_form used in the ObjectListView UI.
        """
        factory_formset_params = {}
        filterset = None
        if filterset_class:
            filterset = filterset_class()
            factory_formset_params = convert_querydict_to_factory_formset_acceptable_querydict(request.GET, filterset)
        return DynamicFilterFormSet(filterset=filterset, data=factory_formset_params)

    def construct_user_permissions(self, request, model):
        """
        Helper function to gather the user's permissions to add, change, delete and view the model,
        and then render the action buttons accordingly allowed in the ObjectListView UI.
        """
        permissions = {}
        for action in ("add", "change", "delete", "view"):
            perm_name = get_permission_for_model(model, action)
            permissions[action] = request.user.has_perm(perm_name)
        return permissions

    def construct_table(self, view, **kwargs):
        """
        Helper function to construct and paginate the table for rendering used in the ObjectListView, ObjectBulkUpdateView and ObjectBulkDestroyView.
        """
        table_class = view.get_table_class()
        request = kwargs.get("request", view.request)
        queryset = view.alter_queryset(request)
        if view.action in ["list", "notes", "changelog"]:
            if view.action == "list":
                permissions = kwargs.get("permissions", {})
                table = table_class(queryset, user=request.user)
                if "pk" in table.base_columns and (permissions["change"] or permissions["delete"]):
                    table.columns.show("pk")
            elif view.action == "notes":
                obj = kwargs.get("object")
                table = table_class(obj.notes, user=request.user)
            elif view.action == "changelog":
                obj = kwargs.get("object")
                content_type = kwargs.get("content_type")
                objectchanges = (
                    ObjectChange.objects.restrict(request.user, "view")
                    .prefetch_related("user", "changed_object_type")
                    .filter(
                        Q(changed_object_type=content_type, changed_object_id=obj.pk)
                        | Q(related_object_type=content_type, related_object_id=obj.pk)
                    )
                )
                table = table_class(data=objectchanges, orderable=False)

            # Apply the request context
            paginate = {
                "paginator_class": EnhancedPaginator,
                "per_page": get_paginate_count(request),
            }
            max_page_size = get_settings_or_config("MAX_PAGE_SIZE")
            if max_page_size and paginate["per_page"] > max_page_size:
                messages.warning(
                    request,
                    f'Requested "per_page" is too large. No more than {max_page_size} items may be displayed at a time.',
                )
            return RequestConfig(request, paginate).configure(table)
        else:
            pk_list = kwargs.get("pk_list", [])
            table = table_class(queryset.filter(pk__in=pk_list), orderable=False)
            if view.action in ["bulk_destroy", "bulk_update"]:
                # Hide actions column if present
                if "actions" in table.columns:
                    table.columns.hide("actions")
            return table

    def validate_action_buttons(self, view, request):
        """Verify actions in self.action_buttons are valid view actions."""
        queryset = view.alter_queryset(request)
        always_valid_actions = ("export",)
        valid_actions = []
        invalid_actions = []
        # added check for whether the action_buttons exist because of issue #2107
        if view.action_buttons is None:
            view.action_buttons = []
        for action in view.action_buttons:
            if action in always_valid_actions or validated_viewname(queryset.model, action) is not None:
                valid_actions.append(action)
            else:
                invalid_actions.append(action)
        if invalid_actions:
            messages.error(request, f"Missing views for action(s) {', '.join(invalid_actions)}")
        return valid_actions

    def get_context(self, data, accepted_media_type, renderer_context):
        """
        Override get_context() from BrowsableAPIRenderer to obtain the context data we need to render our templates.
        context variable contains template context needed to render Nautobot generic templates / circuits templates.
        Override this function to add additional key/value pair to pass it to your templates.
        """
        if renderer_context is None:
            # renderer_context content is automatically provided with the view returning the Response({}) object.
            # The only way renderer_context is None if the user directly calls it from the renderer without a view.
            self.logger.debug(
                "renderer_context is None, please do not directly call get_context() from NautobotHTMLRenderer without specifying the view."
            )
            return {}
        view = renderer_context["view"]
        request = renderer_context["request"]
        # Check if queryset attribute is set before doing anything
        queryset = view.alter_queryset(request)
        model = queryset.model
        form_class = view.get_form_class()
        content_type = ContentType.objects.get_for_model(model)
        form = None
        table = None
        search_form = None
        instance = None
        filter_form = None
        display_filter_params = []
        # Compile a dictionary indicating which permissions are available to the current user for this model
        permissions = self.construct_user_permissions(request, model)
        if view.action in ["create", "retrieve", "update", "destroy", "changelog", "notes"]:
            instance = view.get_object()
            return_url = view.get_return_url(request, instance)
        else:
            return_url = view.get_return_url(request)
        # Get form for context rendering according to view.action unless it is previously set.
        # A form will be passed in from the views if the form has errors.
        if data.get("form"):
            form = data["form"]
        else:
            if view.action == "list":
                if view.filterset_class is not None:
                    view.queryset = view.filter_queryset(queryset)
                    if view.filterset is not None:
                        filterset_filters = view.filterset.filters
                    else:
                        filterset_filters = view.filterset.get_filters()
                    display_filter_params = [
                        check_filter_for_display(filterset_filters, field_name, values)
                        for field_name, values in view.filter_params.items()
                    ]
                    if view.filterset_form_class is not None:
                        filter_form = view.filterset_form_class(request.GET, label_suffix="")
                table = self.construct_table(view, request=request, permissions=permissions)
                search_form = SearchForm(data=request.GET)
            elif view.action == "destroy":
                form = form_class(initial=request.GET)
            elif view.action in ["create", "update"]:
                initial_data = normalize_querydict(request.GET, form_class=form_class)
                form = form_class(instance=instance, initial=initial_data)
                restrict_form_fields(form, request.user)
            elif view.action == "bulk_destroy":
                pk_list = getattr(view, "pk_list", [])
                if pk_list:
                    initial = {
                        "pk": pk_list,
                        "return_url": return_url,
                    }
                    form = form_class(initial=initial)
                table = self.construct_table(view, pk_list=pk_list)
            elif view.action == "bulk_create":
                form = view.get_form()
                if request.data:
                    table = data.get("table")
            elif view.action == "bulk_update":
                pk_list = getattr(view, "pk_list", [])
                if pk_list:
                    initial_data = {"pk": pk_list}
                    form = form_class(model, initial=initial_data)

                    restrict_form_fields(form, request.user)
                table = self.construct_table(view, pk_list=pk_list)
            elif view.action == "notes":
                initial_data = {
                    "assigned_object_type": content_type,
                    "assigned_object_id": instance.pk,
                }
                form = form_class(initial=initial_data)
                table = self.construct_table(view, object=instance)
            elif view.action == "changelog":
                table = self.construct_table(view, object=instance, content_type=content_type)

        context = {
            "content_type": content_type,
            "form": form,
            "filter_form": filter_form,
            "dynamic_filter_form": self.get_dynamic_filter_form(view, request, filterset_class=view.filterset_class),
            "search_form": search_form,
            "filter_params": display_filter_params,
            "object": instance,
            "obj": instance,  # NOTE: This context key is deprecated in favor of `object`.
            "obj_type": queryset.model._meta.verbose_name,  # NOTE: This context key is deprecated in favor of `verbose_name`.
            "obj_type_plural": queryset.model._meta.verbose_name_plural,  # NOTE: This context key is deprecated in favor of `verbose_name_plural`.
            "permissions": permissions,
            "return_url": return_url,
            "table": table if table is not None else data.get("table", None),
            "table_config_form": TableConfigForm(table=table) if table else None,
            "verbose_name": queryset.model._meta.verbose_name,
            "verbose_name_plural": queryset.model._meta.verbose_name_plural,
        }
        if view.action == "retrieve":
            created_by, last_updated_by = get_created_and_last_updated_usernames_for_model(instance)

            context["created_by"] = created_by
            context["last_updated_by"] = last_updated_by
            context.update(view.get_extra_context(request, instance))
        else:
            if view.action == "list":
                # Construct valid actions for list view.
                valid_actions = self.validate_action_buttons(view, request)
                context.update(
                    {
                        "action_buttons": valid_actions,
                        "list_url": validated_viewname(model, "list"),
                        "title": bettertitle(model._meta.verbose_name_plural),
                    }
                )
            elif view.action in ["create", "update"]:
                context.update(
                    {
                        "editing": instance.present_in_database,
                    }
                )
            elif view.action == "bulk_create":
                context.update(
                    {
                        "active_tab": view.bulk_create_active_tab if view.bulk_create_active_tab else "csv-data",
                        "fields": get_csv_form_fields_from_serializer_class(view.serializer_class),
                    }
                )
            elif view.action in ["changelog", "notes"]:
                context.update(
                    {
                        "base_template": get_base_template(data.get("base_template"), model),
                        "active_tab": view.action,
                    }
                )
            context.update(view.get_extra_context(request, instance=None))
        return context

    def render(self, data, accepted_media_type=None, renderer_context=None):
        """
        Overrode render() from BrowsableAPIRenderer to set self.template with NautobotViewSet's get_template_name() before it is rendered.
        """
        view = renderer_context["view"]
        # Get the corresponding template based on self.action in view.get_template_name() unless it is already specified in the Response() data.
        # See form_valid() for self.action == "bulk_create".
        self.template = data.get("template", view.get_template_name())

        # NautobotUIViewSets pass "use_new_ui" in context as they share the same class and are just different methods
        self.use_new_ui = data.get("use_new_ui", False)
        return super().render(data, accepted_media_type=accepted_media_type, renderer_context=renderer_context)

construct_table(view, **kwargs)

Helper function to construct and paginate the table for rendering used in the ObjectListView, ObjectBulkUpdateView and ObjectBulkDestroyView.

Source code in nautobot/core/views/renderers.py
def construct_table(self, view, **kwargs):
    """
    Helper function to construct and paginate the table for rendering used in the ObjectListView, ObjectBulkUpdateView and ObjectBulkDestroyView.
    """
    table_class = view.get_table_class()
    request = kwargs.get("request", view.request)
    queryset = view.alter_queryset(request)
    if view.action in ["list", "notes", "changelog"]:
        if view.action == "list":
            permissions = kwargs.get("permissions", {})
            table = table_class(queryset, user=request.user)
            if "pk" in table.base_columns and (permissions["change"] or permissions["delete"]):
                table.columns.show("pk")
        elif view.action == "notes":
            obj = kwargs.get("object")
            table = table_class(obj.notes, user=request.user)
        elif view.action == "changelog":
            obj = kwargs.get("object")
            content_type = kwargs.get("content_type")
            objectchanges = (
                ObjectChange.objects.restrict(request.user, "view")
                .prefetch_related("user", "changed_object_type")
                .filter(
                    Q(changed_object_type=content_type, changed_object_id=obj.pk)
                    | Q(related_object_type=content_type, related_object_id=obj.pk)
                )
            )
            table = table_class(data=objectchanges, orderable=False)

        # Apply the request context
        paginate = {
            "paginator_class": EnhancedPaginator,
            "per_page": get_paginate_count(request),
        }
        max_page_size = get_settings_or_config("MAX_PAGE_SIZE")
        if max_page_size and paginate["per_page"] > max_page_size:
            messages.warning(
                request,
                f'Requested "per_page" is too large. No more than {max_page_size} items may be displayed at a time.',
            )
        return RequestConfig(request, paginate).configure(table)
    else:
        pk_list = kwargs.get("pk_list", [])
        table = table_class(queryset.filter(pk__in=pk_list), orderable=False)
        if view.action in ["bulk_destroy", "bulk_update"]:
            # Hide actions column if present
            if "actions" in table.columns:
                table.columns.hide("actions")
        return table

construct_user_permissions(request, model)

Helper function to gather the user's permissions to add, change, delete and view the model, and then render the action buttons accordingly allowed in the ObjectListView UI.

Source code in nautobot/core/views/renderers.py
def construct_user_permissions(self, request, model):
    """
    Helper function to gather the user's permissions to add, change, delete and view the model,
    and then render the action buttons accordingly allowed in the ObjectListView UI.
    """
    permissions = {}
    for action in ("add", "change", "delete", "view"):
        perm_name = get_permission_for_model(model, action)
        permissions[action] = request.user.has_perm(perm_name)
    return permissions

get_context(data, accepted_media_type, renderer_context)

Override get_context() from BrowsableAPIRenderer to obtain the context data we need to render our templates. context variable contains template context needed to render Nautobot generic templates / circuits templates. Override this function to add additional key/value pair to pass it to your templates.

Source code in nautobot/core/views/renderers.py
def get_context(self, data, accepted_media_type, renderer_context):
    """
    Override get_context() from BrowsableAPIRenderer to obtain the context data we need to render our templates.
    context variable contains template context needed to render Nautobot generic templates / circuits templates.
    Override this function to add additional key/value pair to pass it to your templates.
    """
    if renderer_context is None:
        # renderer_context content is automatically provided with the view returning the Response({}) object.
        # The only way renderer_context is None if the user directly calls it from the renderer without a view.
        self.logger.debug(
            "renderer_context is None, please do not directly call get_context() from NautobotHTMLRenderer without specifying the view."
        )
        return {}
    view = renderer_context["view"]
    request = renderer_context["request"]
    # Check if queryset attribute is set before doing anything
    queryset = view.alter_queryset(request)
    model = queryset.model
    form_class = view.get_form_class()
    content_type = ContentType.objects.get_for_model(model)
    form = None
    table = None
    search_form = None
    instance = None
    filter_form = None
    display_filter_params = []
    # Compile a dictionary indicating which permissions are available to the current user for this model
    permissions = self.construct_user_permissions(request, model)
    if view.action in ["create", "retrieve", "update", "destroy", "changelog", "notes"]:
        instance = view.get_object()
        return_url = view.get_return_url(request, instance)
    else:
        return_url = view.get_return_url(request)
    # Get form for context rendering according to view.action unless it is previously set.
    # A form will be passed in from the views if the form has errors.
    if data.get("form"):
        form = data["form"]
    else:
        if view.action == "list":
            if view.filterset_class is not None:
                view.queryset = view.filter_queryset(queryset)
                if view.filterset is not None:
                    filterset_filters = view.filterset.filters
                else:
                    filterset_filters = view.filterset.get_filters()
                display_filter_params = [
                    check_filter_for_display(filterset_filters, field_name, values)
                    for field_name, values in view.filter_params.items()
                ]
                if view.filterset_form_class is not None:
                    filter_form = view.filterset_form_class(request.GET, label_suffix="")
            table = self.construct_table(view, request=request, permissions=permissions)
            search_form = SearchForm(data=request.GET)
        elif view.action == "destroy":
            form = form_class(initial=request.GET)
        elif view.action in ["create", "update"]:
            initial_data = normalize_querydict(request.GET, form_class=form_class)
            form = form_class(instance=instance, initial=initial_data)
            restrict_form_fields(form, request.user)
        elif view.action == "bulk_destroy":
            pk_list = getattr(view, "pk_list", [])
            if pk_list:
                initial = {
                    "pk": pk_list,
                    "return_url": return_url,
                }
                form = form_class(initial=initial)
            table = self.construct_table(view, pk_list=pk_list)
        elif view.action == "bulk_create":
            form = view.get_form()
            if request.data:
                table = data.get("table")
        elif view.action == "bulk_update":
            pk_list = getattr(view, "pk_list", [])
            if pk_list:
                initial_data = {"pk": pk_list}
                form = form_class(model, initial=initial_data)

                restrict_form_fields(form, request.user)
            table = self.construct_table(view, pk_list=pk_list)
        elif view.action == "notes":
            initial_data = {
                "assigned_object_type": content_type,
                "assigned_object_id": instance.pk,
            }
            form = form_class(initial=initial_data)
            table = self.construct_table(view, object=instance)
        elif view.action == "changelog":
            table = self.construct_table(view, object=instance, content_type=content_type)

    context = {
        "content_type": content_type,
        "form": form,
        "filter_form": filter_form,
        "dynamic_filter_form": self.get_dynamic_filter_form(view, request, filterset_class=view.filterset_class),
        "search_form": search_form,
        "filter_params": display_filter_params,
        "object": instance,
        "obj": instance,  # NOTE: This context key is deprecated in favor of `object`.
        "obj_type": queryset.model._meta.verbose_name,  # NOTE: This context key is deprecated in favor of `verbose_name`.
        "obj_type_plural": queryset.model._meta.verbose_name_plural,  # NOTE: This context key is deprecated in favor of `verbose_name_plural`.
        "permissions": permissions,
        "return_url": return_url,
        "table": table if table is not None else data.get("table", None),
        "table_config_form": TableConfigForm(table=table) if table else None,
        "verbose_name": queryset.model._meta.verbose_name,
        "verbose_name_plural": queryset.model._meta.verbose_name_plural,
    }
    if view.action == "retrieve":
        created_by, last_updated_by = get_created_and_last_updated_usernames_for_model(instance)

        context["created_by"] = created_by
        context["last_updated_by"] = last_updated_by
        context.update(view.get_extra_context(request, instance))
    else:
        if view.action == "list":
            # Construct valid actions for list view.
            valid_actions = self.validate_action_buttons(view, request)
            context.update(
                {
                    "action_buttons": valid_actions,
                    "list_url": validated_viewname(model, "list"),
                    "title": bettertitle(model._meta.verbose_name_plural),
                }
            )
        elif view.action in ["create", "update"]:
            context.update(
                {
                    "editing": instance.present_in_database,
                }
            )
        elif view.action == "bulk_create":
            context.update(
                {
                    "active_tab": view.bulk_create_active_tab if view.bulk_create_active_tab else "csv-data",
                    "fields": get_csv_form_fields_from_serializer_class(view.serializer_class),
                }
            )
        elif view.action in ["changelog", "notes"]:
            context.update(
                {
                    "base_template": get_base_template(data.get("base_template"), model),
                    "active_tab": view.action,
                }
            )
        context.update(view.get_extra_context(request, instance=None))
    return context

get_dynamic_filter_form(view, request, *args, filterset_class=None, **kwargs)

Helper function to obtain the filter_form_class, and then initialize and return the filter_form used in the ObjectListView UI.

Source code in nautobot/core/views/renderers.py
def get_dynamic_filter_form(self, view, request, *args, filterset_class=None, **kwargs):
    """
    Helper function to obtain the filter_form_class,
    and then initialize and return the filter_form used in the ObjectListView UI.
    """
    factory_formset_params = {}
    filterset = None
    if filterset_class:
        filterset = filterset_class()
        factory_formset_params = convert_querydict_to_factory_formset_acceptable_querydict(request.GET, filterset)
    return DynamicFilterFormSet(filterset=filterset, data=factory_formset_params)

render(data, accepted_media_type=None, renderer_context=None)

Overrode render() from BrowsableAPIRenderer to set self.template with NautobotViewSet's get_template_name() before it is rendered.

Source code in nautobot/core/views/renderers.py
def render(self, data, accepted_media_type=None, renderer_context=None):
    """
    Overrode render() from BrowsableAPIRenderer to set self.template with NautobotViewSet's get_template_name() before it is rendered.
    """
    view = renderer_context["view"]
    # Get the corresponding template based on self.action in view.get_template_name() unless it is already specified in the Response() data.
    # See form_valid() for self.action == "bulk_create".
    self.template = data.get("template", view.get_template_name())

    # NautobotUIViewSets pass "use_new_ui" in context as they share the same class and are just different methods
    self.use_new_ui = data.get("use_new_ui", False)
    return super().render(data, accepted_media_type=accepted_media_type, renderer_context=renderer_context)

validate_action_buttons(view, request)

Verify actions in self.action_buttons are valid view actions.

Source code in nautobot/core/views/renderers.py
def validate_action_buttons(self, view, request):
    """Verify actions in self.action_buttons are valid view actions."""
    queryset = view.alter_queryset(request)
    always_valid_actions = ("export",)
    valid_actions = []
    invalid_actions = []
    # added check for whether the action_buttons exist because of issue #2107
    if view.action_buttons is None:
        view.action_buttons = []
    for action in view.action_buttons:
        if action in always_valid_actions or validated_viewname(queryset.model, action) is not None:
            valid_actions.append(action)
        else:
            invalid_actions.append(action)
    if invalid_actions:
        messages.error(request, f"Missing views for action(s) {', '.join(invalid_actions)}")
    return valid_actions

nautobot.apps.views.NautobotUIViewSet

Bases: mixins.ObjectDetailViewMixin, mixins.ObjectListViewMixin, mixins.ObjectEditViewMixin, mixins.ObjectDestroyViewMixin, mixins.ObjectBulkDestroyViewMixin, mixins.ObjectBulkCreateViewMixin, mixins.ObjectBulkUpdateViewMixin, mixins.ObjectChangeLogViewMixin, mixins.ObjectNotesViewMixin

Nautobot BaseViewSet that is intended for UI use only. It provides default Nautobot functionalities such as create(), bulk_create(), update(), partial_update(), bulk_update(), destroy(), bulk_destroy(), retrieve() notes(), changelog() and list() actions.

Source code in nautobot/core/views/viewsets.py
class NautobotUIViewSet(
    mixins.ObjectDetailViewMixin,
    mixins.ObjectListViewMixin,
    mixins.ObjectEditViewMixin,
    mixins.ObjectDestroyViewMixin,
    mixins.ObjectBulkDestroyViewMixin,
    mixins.ObjectBulkCreateViewMixin,
    mixins.ObjectBulkUpdateViewMixin,
    mixins.ObjectChangeLogViewMixin,
    mixins.ObjectNotesViewMixin,
):
    """
    Nautobot BaseViewSet that is intended for UI use only. It provides default Nautobot functionalities such as
    `create()`, `bulk_create()`, `update()`, `partial_update()`, `bulk_update()`, `destroy()`, `bulk_destroy()`, `retrieve()`
    `notes()`, `changelog()` and `list()` actions.
    """

nautobot.apps.views.NautobotViewSetMixin

Bases: GenericViewSet, AccessMixin, GetReturnURLMixin, FormView

NautobotViewSetMixin is an aggregation of various mixins from DRF, Django and Nautobot to acheive the desired behavior pattern for NautobotUIViewSet

Source code in nautobot/core/views/mixins.py
@extend_schema(exclude=True)
class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormView):
    """
    NautobotViewSetMixin is an aggregation of various mixins from DRF, Django and Nautobot to acheive the desired behavior pattern for NautobotUIViewSet
    """

    renderer_classes = [NautobotHTMLRenderer]
    logger = logging.getLogger(__name__)
    # Attributes that need to be specified: form_class, queryset, serializer_class, table_class for most mixins.
    # filterset and filter_params will be initialized in filter_queryset() in ObjectListViewMixin
    filter_params = None
    filterset = None
    filterset_class = None
    filterset_form_class = None
    form_class = None
    create_form_class = None
    update_form_class = None
    parser_classes = [FormParser, MultiPartParser]
    queryset = None
    # serializer_class has to be specified to eliminate the need to override retrieve() in the RetrieveModelMixin for now.
    serializer_class = None
    table_class = None
    notes_form_class = NoteForm

    def get_permissions_for_model(self, model, actions):
        """
        Resolve the named permissions for a given model (or instance) and a list of actions (e.g. view or add).

        :param model: A model or instance
        :param actions: A list of actions to perform on the model
        """
        model_permissions = []
        for action in actions:
            model_permissions.append(f"{model._meta.app_label}.{action}_{model._meta.model_name}")
        return model_permissions

    def get_required_permission(self):
        """
        Obtain the permissions needed to perform certain actions on a model.
        """
        queryset = self.get_queryset()
        try:
            actions = [self.get_action()]
        except KeyError:
            messages.error(
                self.request,
                "This action is not permitted. Please use the buttons at the bottom of the table for Bulk Delete and Bulk Update",
            )
        return self.get_permissions_for_model(queryset.model, actions)

    def check_permissions(self, request):
        """
        Check whether the user has the permissions needed to perform certain actions.
        """
        user = self.request.user
        permission_required = self.get_required_permission()
        # Check that the user has been granted the required permission(s) one by one.
        # In case the permission has `message` or `code`` attribute, we want to include those information in the permission_denied error.
        for permission in permission_required:
            # If the user does not have the permission required, we raise DRF's `NotAuthenticated` or `PermissionDenied` exception
            # which will be handled by self.handle_no_permission() in the UI appropriately in the dispatch() method
            # Cast permission to a list since has_perms() takes a list type parameter.
            if not user.has_perms([permission]):
                self.permission_denied(
                    request,
                    message=getattr(permission, "message", None),
                    code=getattr(permission, "code", None),
                )

    def dispatch(self, request, *args, **kwargs):
        """
        Override the default dispatch() method to check permissions first.
        Used to determine whether the user has permissions to a view and object-level permissions.
        Using AccessMixin handle_no_permission() to deal with Object-Level permissions and API-Level permissions in one pass.
        """
        # self.initialize_request() converts a WSGI request and returns an API request object which can be passed into self.check_permissions()
        # If the user is not authenticated or does not have the permission to perform certain actions,
        # DRF NotAuthenticated or PermissionDenied exception can be raised appropriately and handled by self.handle_no_permission() in the UI.
        # initialize_request() also instantiates self.action which is needed for permission checks.
        api_request = self.initialize_request(request, *args, **kwargs)
        try:
            self.check_permissions(api_request)
        # check_permissions() could raise NotAuthenticated and PermissionDenied Error.
        # We handle them by a single except statement since self.handle_no_permission() is able to handle both errors
        except (exceptions.NotAuthenticated, exceptions.PermissionDenied):
            return self.handle_no_permission()

        return super().dispatch(request, *args, **kwargs)

    def get_table_class(self):
        # Check if self.table_class is specified in the ModelViewSet before performing subsequent actions
        # If not, display an error message
        if self.action == "notes":
            return NoteTable
        elif self.action == "changelog":
            return ObjectChangeTable

        if self.table_class is None:
            raise NotImplementedError(
                f"'{self.__class__.__name__}' should include a `table_class` attribute for bulk operations"
            )

        return self.table_class

    def _process_destroy_form(self, form):
        """
        Helper method to destroy an object after the form is validated successfully.
        """
        raise NotImplementedError("_process_destroy_form() is not implemented")

    def _process_bulk_destroy_form(self, form):
        """
        Helper method to destroy objects after the form is validated successfully.
        """
        raise NotImplementedError("_process_bulk_destroy_form() is not implemented")

    def _process_create_or_update_form(self, form):
        """
        Helper method to create or update an object after the form is validated successfully.
        """
        raise NotImplementedError("_process_create_or_update_form() is not implemented")

    def _process_bulk_update_form(self, form):
        """
        Helper method to edit objects in bulk after the form is validated successfully.
        """
        raise NotImplementedError("_process_bulk_update_form() is not implemented")

    def _process_bulk_create_form(self, form):
        """
        Helper method to create objects in bulk after the form is validated successfully.
        """
        raise NotImplementedError("_process_bulk_create_form() is not implemented")

    def _handle_object_does_not_exist(self, form):
        msg = "Object import failed due to object-level permissions violation"
        self.logger.debug(msg)
        self.has_error = True
        form.add_error(None, msg)
        return form

    def _handle_not_implemented_error(self):
        # Blanket handler for NotImplementedError raised by form helper functions
        msg = "Please provide the appropriate mixin before using this helper function"
        messages.error(self.request, msg)
        self.has_error = True

    def _handle_validation_error(self, e):
        # For bulk_create/bulk_update view, self.obj is not set since there are multiple
        # The errors will be rendered on the form itself.
        if self.action not in ["bulk_create", "bulk_update"]:
            messages.error(self.request, f"{self.obj} failed validation: {e}")
        self.has_error = True

    def form_valid(self, form):
        """
        Handle valid forms and redirect to success_url.
        """
        request = self.request
        self.has_error = False
        queryset = self.get_queryset()
        try:
            if self.action == "destroy":
                self._process_destroy_form(form)
            elif self.action == "bulk_destroy":
                self._process_bulk_destroy_form(form)
            elif self.action in ["create", "update"]:
                self._process_create_or_update_form(form)
            elif self.action == "bulk_update":
                self._process_bulk_update_form(form)
            elif self.action == "bulk_create":
                self.obj_table = self._process_bulk_create_form(form)
        except ValidationError as e:
            self._handle_validation_error(e)
        except ObjectDoesNotExist:
            form = self._handle_object_does_not_exist(form)
        except NotImplementedError:
            self._handle_not_implemented_error()

        if not self.has_error:
            self.logger.debug("Form validation was successful")
            if self.action == "bulk_create":
                return Response(
                    {
                        "table": self.obj_table,
                        "template": "import_success.html",
                    }
                )
            return super().form_valid(form)
        else:
            # render the form with the error message.
            data = {}
            if self.action in ["bulk_update", "bulk_destroy"]:
                pk_list = self.pk_list
                table_class = self.get_table_class()
                table = table_class(queryset.filter(pk__in=pk_list), orderable=False)
                if not table.rows:
                    messages.warning(
                        request,
                        f"No {queryset.model._meta.verbose_name_plural} were selected for {self.action}.",
                    )
                    return redirect(self.get_return_url(request))

                data.update({"table": table})
            data.update({"form": form})
            return Response(data)

    def form_invalid(self, form):
        """
        Handle invalid forms.
        """
        data = {}
        request = self.request
        queryset = self.get_queryset()
        if self.action in ["bulk_update", "bulk_destroy"]:
            pk_list = self.pk_list
            table_class = self.get_table_class()
            table = table_class(queryset.filter(pk__in=pk_list), orderable=False)
            if not table.rows:
                messages.warning(
                    request,
                    f"No {queryset.model._meta.verbose_name_plural} were selected for {self.action}.",
                )
                return redirect(self.get_return_url(request))

            data = {
                "table": table,
            }
        data.update({"form": form})
        return Response(data)

    def get_object(self):
        """
        Returns the object the view is displaying.
        You may want to override this if you need to provide non-standard
        queryset lookups.  Eg if objects are referenced using multiple
        keyword arguments in the url conf.
        """
        queryset = self.get_queryset()
        # Perform the lookup filtering.
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
        if lookup_url_kwarg not in self.kwargs:
            return queryset.model()
        filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
        obj = get_object_or_404(queryset, **filter_kwargs)

        return obj

    def get_filter_params(self, request):
        """Helper function - take request.GET and discard any parameters that are not used for queryset filtering."""
        filter_params = request.GET.copy()
        return get_filterable_params_from_filter_params(filter_params, self.non_filter_params, self.filterset_class())

    def get_queryset(self):
        """
        Get the list of items for this view.
        This must be an iterable, and may be a queryset.
        Defaults to using `self.queryset`.
        This method should always be used rather than accessing `self.queryset`
        directly, as `self.queryset` gets evaluated only once, and those results
        are cached for all subsequent requests.
        Override the original `get_queryset()` to apply permission specific to the user and action.
        """
        queryset = super().get_queryset()
        return queryset.restrict(self.request.user, self.get_action())

    def get_action(self):
        """Helper method for retrieving action and if action not set defaulting to action name."""
        return PERMISSIONS_ACTION_MAP.get(self.action, self.action)

    def get_extra_context(self, request, instance=None):
        """
        Return any additional context data for the template.
        request: The current request
        instance: The object being viewed
        """
        return {}

    def get_template_name(self):
        # Use "<app>/<model>_<action> if available, else fall back to generic templates
        queryset = self.get_queryset()
        model_opts = queryset.model._meta
        app_label = model_opts.app_label
        action = self.action

        try:
            template_name = f"{app_label}/{model_opts.model_name}_{action}.html"
            select_template([template_name])
        except TemplateDoesNotExist:
            try:
                if action == "create":
                    # When the action is `create`, try {object}_update.html as a fallback
                    # If both are not defined, fall back to generic/object_create.html
                    template_name = f"{app_label}/{model_opts.model_name}_update.html"
                    select_template([template_name])
                elif action == "update":
                    # When the action is `update`, try {object}_create.html as a fallback
                    # If both are not defined, fall back to generic/object_update.html
                    template_name = f"{app_label}/{model_opts.model_name}_create.html"
                    select_template([template_name])
                else:
                    # No special case fallback, fall back to generic/object_{action}.html
                    raise TemplateDoesNotExist("")
            except TemplateDoesNotExist:
                template_name = f"generic/object_{action}.html"
        return template_name

    def get_form(self, *args, **kwargs):
        """
        Helper function to get form for different views if specified.
        If not, return instantiated form using form_class.
        """
        form = getattr(self, f"{self.action}_form", None)
        if not form:
            form_class = self.get_form_class()
            if not form_class:
                self.logger.debug(f"{self.action}_form_class is not defined")
                return None
            form = form_class(*args, **kwargs)
        return form

    def get_form_class(self, **kwargs):
        """
        Helper function to get form_class for different views.
        """

        if self.action in ["create", "update"]:
            if getattr(self, f"{self.action}_form_class"):
                form_class = getattr(self, f"{self.action}_form_class")
            else:
                form_class = getattr(self, "form_class", None)
        elif self.action == "bulk_create":
            required_field_names = [
                field["name"]
                for field in get_csv_form_fields_from_serializer_class(self.serializer_class)
                if field["required"]
            ]

            class BulkCreateForm(BootstrapMixin, Form):
                csv_data = CSVDataField(required_field_names=required_field_names)
                csv_file = CSVFileField()

            form_class = BulkCreateForm
        else:
            form_class = getattr(self, f"{self.action}_form_class", None)

        if not form_class:
            if self.action == "bulk_destroy":
                queryset = self.get_queryset()

                class BulkDestroyForm(ConfirmationForm):
                    pk = ModelMultipleChoiceField(queryset=queryset, widget=MultipleHiddenInput)

                return BulkDestroyForm
            else:
                # Check for request first and then kwargs for form_class specified.
                form_class = self.request.data.get("form_class", None)
                if not form_class:
                    form_class = kwargs.get("form_class", None)
        return form_class

    def form_save(self, form, **kwargs):
        """
        Generic method to save the object from form.
        Should be overriden by user if customization is needed.
        """
        return form.save()

    def alter_queryset(self, request):
        # .all() is necessary to avoid caching queries
        queryset = self.get_queryset()
        return queryset.all()

check_permissions(request)

Check whether the user has the permissions needed to perform certain actions.

Source code in nautobot/core/views/mixins.py
def check_permissions(self, request):
    """
    Check whether the user has the permissions needed to perform certain actions.
    """
    user = self.request.user
    permission_required = self.get_required_permission()
    # Check that the user has been granted the required permission(s) one by one.
    # In case the permission has `message` or `code`` attribute, we want to include those information in the permission_denied error.
    for permission in permission_required:
        # If the user does not have the permission required, we raise DRF's `NotAuthenticated` or `PermissionDenied` exception
        # which will be handled by self.handle_no_permission() in the UI appropriately in the dispatch() method
        # Cast permission to a list since has_perms() takes a list type parameter.
        if not user.has_perms([permission]):
            self.permission_denied(
                request,
                message=getattr(permission, "message", None),
                code=getattr(permission, "code", None),
            )

dispatch(request, *args, **kwargs)

Override the default dispatch() method to check permissions first. Used to determine whether the user has permissions to a view and object-level permissions. Using AccessMixin handle_no_permission() to deal with Object-Level permissions and API-Level permissions in one pass.

Source code in nautobot/core/views/mixins.py
def dispatch(self, request, *args, **kwargs):
    """
    Override the default dispatch() method to check permissions first.
    Used to determine whether the user has permissions to a view and object-level permissions.
    Using AccessMixin handle_no_permission() to deal with Object-Level permissions and API-Level permissions in one pass.
    """
    # self.initialize_request() converts a WSGI request and returns an API request object which can be passed into self.check_permissions()
    # If the user is not authenticated or does not have the permission to perform certain actions,
    # DRF NotAuthenticated or PermissionDenied exception can be raised appropriately and handled by self.handle_no_permission() in the UI.
    # initialize_request() also instantiates self.action which is needed for permission checks.
    api_request = self.initialize_request(request, *args, **kwargs)
    try:
        self.check_permissions(api_request)
    # check_permissions() could raise NotAuthenticated and PermissionDenied Error.
    # We handle them by a single except statement since self.handle_no_permission() is able to handle both errors
    except (exceptions.NotAuthenticated, exceptions.PermissionDenied):
        return self.handle_no_permission()

    return super().dispatch(request, *args, **kwargs)

form_invalid(form)

Handle invalid forms.

Source code in nautobot/core/views/mixins.py
def form_invalid(self, form):
    """
    Handle invalid forms.
    """
    data = {}
    request = self.request
    queryset = self.get_queryset()
    if self.action in ["bulk_update", "bulk_destroy"]:
        pk_list = self.pk_list
        table_class = self.get_table_class()
        table = table_class(queryset.filter(pk__in=pk_list), orderable=False)
        if not table.rows:
            messages.warning(
                request,
                f"No {queryset.model._meta.verbose_name_plural} were selected for {self.action}.",
            )
            return redirect(self.get_return_url(request))

        data = {
            "table": table,
        }
    data.update({"form": form})
    return Response(data)

form_save(form, **kwargs)

Generic method to save the object from form. Should be overriden by user if customization is needed.

Source code in nautobot/core/views/mixins.py
def form_save(self, form, **kwargs):
    """
    Generic method to save the object from form.
    Should be overriden by user if customization is needed.
    """
    return form.save()

form_valid(form)

Handle valid forms and redirect to success_url.

Source code in nautobot/core/views/mixins.py
def form_valid(self, form):
    """
    Handle valid forms and redirect to success_url.
    """
    request = self.request
    self.has_error = False
    queryset = self.get_queryset()
    try:
        if self.action == "destroy":
            self._process_destroy_form(form)
        elif self.action == "bulk_destroy":
            self._process_bulk_destroy_form(form)
        elif self.action in ["create", "update"]:
            self._process_create_or_update_form(form)
        elif self.action == "bulk_update":
            self._process_bulk_update_form(form)
        elif self.action == "bulk_create":
            self.obj_table = self._process_bulk_create_form(form)
    except ValidationError as e:
        self._handle_validation_error(e)
    except ObjectDoesNotExist:
        form = self._handle_object_does_not_exist(form)
    except NotImplementedError:
        self._handle_not_implemented_error()

    if not self.has_error:
        self.logger.debug("Form validation was successful")
        if self.action == "bulk_create":
            return Response(
                {
                    "table": self.obj_table,
                    "template": "import_success.html",
                }
            )
        return super().form_valid(form)
    else:
        # render the form with the error message.
        data = {}
        if self.action in ["bulk_update", "bulk_destroy"]:
            pk_list = self.pk_list
            table_class = self.get_table_class()
            table = table_class(queryset.filter(pk__in=pk_list), orderable=False)
            if not table.rows:
                messages.warning(
                    request,
                    f"No {queryset.model._meta.verbose_name_plural} were selected for {self.action}.",
                )
                return redirect(self.get_return_url(request))

            data.update({"table": table})
        data.update({"form": form})
        return Response(data)

get_action()

Helper method for retrieving action and if action not set defaulting to action name.

Source code in nautobot/core/views/mixins.py
def get_action(self):
    """Helper method for retrieving action and if action not set defaulting to action name."""
    return PERMISSIONS_ACTION_MAP.get(self.action, self.action)

get_extra_context(request, instance=None)

Return any additional context data for the template. request: The current request instance: The object being viewed

Source code in nautobot/core/views/mixins.py
def get_extra_context(self, request, instance=None):
    """
    Return any additional context data for the template.
    request: The current request
    instance: The object being viewed
    """
    return {}

get_filter_params(request)

Helper function - take request.GET and discard any parameters that are not used for queryset filtering.

Source code in nautobot/core/views/mixins.py
def get_filter_params(self, request):
    """Helper function - take request.GET and discard any parameters that are not used for queryset filtering."""
    filter_params = request.GET.copy()
    return get_filterable_params_from_filter_params(filter_params, self.non_filter_params, self.filterset_class())

get_form(*args, **kwargs)

Helper function to get form for different views if specified. If not, return instantiated form using form_class.

Source code in nautobot/core/views/mixins.py
def get_form(self, *args, **kwargs):
    """
    Helper function to get form for different views if specified.
    If not, return instantiated form using form_class.
    """
    form = getattr(self, f"{self.action}_form", None)
    if not form:
        form_class = self.get_form_class()
        if not form_class:
            self.logger.debug(f"{self.action}_form_class is not defined")
            return None
        form = form_class(*args, **kwargs)
    return form

get_form_class(**kwargs)

Helper function to get form_class for different views.

Source code in nautobot/core/views/mixins.py
def get_form_class(self, **kwargs):
    """
    Helper function to get form_class for different views.
    """

    if self.action in ["create", "update"]:
        if getattr(self, f"{self.action}_form_class"):
            form_class = getattr(self, f"{self.action}_form_class")
        else:
            form_class = getattr(self, "form_class", None)
    elif self.action == "bulk_create":
        required_field_names = [
            field["name"]
            for field in get_csv_form_fields_from_serializer_class(self.serializer_class)
            if field["required"]
        ]

        class BulkCreateForm(BootstrapMixin, Form):
            csv_data = CSVDataField(required_field_names=required_field_names)
            csv_file = CSVFileField()

        form_class = BulkCreateForm
    else:
        form_class = getattr(self, f"{self.action}_form_class", None)

    if not form_class:
        if self.action == "bulk_destroy":
            queryset = self.get_queryset()

            class BulkDestroyForm(ConfirmationForm):
                pk = ModelMultipleChoiceField(queryset=queryset, widget=MultipleHiddenInput)

            return BulkDestroyForm
        else:
            # Check for request first and then kwargs for form_class specified.
            form_class = self.request.data.get("form_class", None)
            if not form_class:
                form_class = kwargs.get("form_class", None)
    return form_class

get_object()

Returns the object the view is displaying. You may want to override this if you need to provide non-standard queryset lookups. Eg if objects are referenced using multiple keyword arguments in the url conf.

Source code in nautobot/core/views/mixins.py
def get_object(self):
    """
    Returns the object the view is displaying.
    You may want to override this if you need to provide non-standard
    queryset lookups.  Eg if objects are referenced using multiple
    keyword arguments in the url conf.
    """
    queryset = self.get_queryset()
    # Perform the lookup filtering.
    lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
    if lookup_url_kwarg not in self.kwargs:
        return queryset.model()
    filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
    obj = get_object_or_404(queryset, **filter_kwargs)

    return obj

get_permissions_for_model(model, actions)

Resolve the named permissions for a given model (or instance) and a list of actions (e.g. view or add).

:param model: A model or instance :param actions: A list of actions to perform on the model

Source code in nautobot/core/views/mixins.py
def get_permissions_for_model(self, model, actions):
    """
    Resolve the named permissions for a given model (or instance) and a list of actions (e.g. view or add).

    :param model: A model or instance
    :param actions: A list of actions to perform on the model
    """
    model_permissions = []
    for action in actions:
        model_permissions.append(f"{model._meta.app_label}.{action}_{model._meta.model_name}")
    return model_permissions

get_queryset()

Get the list of items for this view. This must be an iterable, and may be a queryset. Defaults to using self.queryset. This method should always be used rather than accessing self.queryset directly, as self.queryset gets evaluated only once, and those results are cached for all subsequent requests. Override the original get_queryset() to apply permission specific to the user and action.

Source code in nautobot/core/views/mixins.py
def get_queryset(self):
    """
    Get the list of items for this view.
    This must be an iterable, and may be a queryset.
    Defaults to using `self.queryset`.
    This method should always be used rather than accessing `self.queryset`
    directly, as `self.queryset` gets evaluated only once, and those results
    are cached for all subsequent requests.
    Override the original `get_queryset()` to apply permission specific to the user and action.
    """
    queryset = super().get_queryset()
    return queryset.restrict(self.request.user, self.get_action())

get_required_permission()

Obtain the permissions needed to perform certain actions on a model.

Source code in nautobot/core/views/mixins.py
def get_required_permission(self):
    """
    Obtain the permissions needed to perform certain actions on a model.
    """
    queryset = self.get_queryset()
    try:
        actions = [self.get_action()]
    except KeyError:
        messages.error(
            self.request,
            "This action is not permitted. Please use the buttons at the bottom of the table for Bulk Delete and Bulk Update",
        )
    return self.get_permissions_for_model(queryset.model, actions)

nautobot.apps.views.ObjectBulkCreateViewMixin

Bases: NautobotViewSetMixin

UI mixin to bulk create model instances.

Source code in nautobot/core/views/mixins.py
class ObjectBulkCreateViewMixin(NautobotViewSetMixin):
    """
    UI mixin to bulk create model instances.
    """

    bulk_create_active_tab = "csv-data"

    def _process_bulk_create_form(self, form):
        # Iterate through CSV data and bind each row to a new model form instance.
        new_objs = []
        request = self.request
        queryset = self.get_queryset()
        with transaction.atomic():
            if request.FILES:
                # Set the bulk_create_active_tab to "csv-file"
                # In case the form validation fails, the user will be redirected
                # to the tab with errors rendered on the form.
                self.bulk_create_active_tab = "csv-file"
            new_objs = import_csv_helper(request=request, form=form, serializer_class=self.serializer_class)

            # Enforce object-level permissions
            if queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
                raise ObjectDoesNotExist

        # Compile a table containing the imported objects
        table_class = self.get_table_class()
        obj_table = table_class(new_objs)
        if new_objs:
            msg = f"Imported {len(new_objs)} {new_objs[0]._meta.verbose_name_plural}"
            self.logger.info(msg)
            messages.success(request, msg)
        return obj_table

    def bulk_create(self, request, *args, **kwargs):
        context = {}
        if request.method == "POST":
            return self.perform_bulk_create(request)
        return Response(context)

    def perform_bulk_create(self, request):
        form_class = self.get_form_class()
        form = form_class(request.POST, request.FILES)
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

nautobot.apps.views.ObjectBulkDestroyViewMixin

Bases: NautobotViewSetMixin, BulkDestroyModelMixin

UI mixin to bulk destroy model instances.

Source code in nautobot/core/views/mixins.py
class ObjectBulkDestroyViewMixin(NautobotViewSetMixin, BulkDestroyModelMixin):
    """
    UI mixin to bulk destroy model instances.
    """

    bulk_destroy_form_class = None
    filterset_class = None

    def _process_bulk_destroy_form(self, form):
        request = self.request
        pk_list = self.pk_list
        queryset = self.get_queryset()
        model = queryset.model
        # Delete objects
        queryset = queryset.filter(pk__in=pk_list)

        try:
            with transaction.atomic():
                deleted_count = queryset.delete()[1][model._meta.label]
                msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
                self.logger.info(msg)
                self.success_url = self.get_return_url(request)
                messages.success(request, msg)
        except ProtectedError as e:
            self.logger.info("Caught ProtectedError while attempting to delete objects")
            handle_protectederror(queryset, request, e)
            self.success_url = self.get_return_url(request)

    def bulk_destroy(self, request, *args, **kwargs):
        """
        Call perform_bulk_destroy().
        The function exist to keep the DRF's get/post pattern of {action}/perform_{action}, we will need it when we transition from using forms to serializers in the UI.
        User should override this function to handle any actions as needed before bulk destroy.
        """
        return self.perform_bulk_destroy(request, **kwargs)

    def perform_bulk_destroy(self, request, **kwargs):
        """
        request.POST "_delete": Function to render the user selection of objects in a table form/BulkDestroyConfirmationForm via Response that is passed to NautobotHTMLRenderer.
        request.POST "_confirm": Function to validate the table form/BulkDestroyConfirmationForm and to perform the action of bulk destroy. Render the form with errors if exceptions are raised.
        """
        queryset = self.get_queryset()
        model = queryset.model
        # Are we deleting *all* objects in the queryset or just a selected subset?
        if request.POST.get("_all"):
            filter_params = self.get_filter_params(request)
            if not filter_params:
                self.pk_list = model.objects.only("pk").all().values_list("pk", flat=True)
            elif self.filterset_class is None:
                raise NotImplementedError("filterset_class must be defined to use _all")
            else:
                self.pk_list = self.filterset_class(filter_params, model.objects.only("pk")).qs
        else:
            self.pk_list = request.POST.getlist("pk")
        form_class = self.get_form_class(**kwargs)
        data = {}
        if "_confirm" in request.POST:
            form = form_class(request.POST)
            if form.is_valid():
                return self.form_valid(form)
            else:
                return self.form_invalid(form)
        table_class = self.get_table_class()
        table = table_class(queryset.filter(pk__in=self.pk_list), orderable=False)
        if not table.rows:
            messages.warning(
                request,
                f"No {queryset.model._meta.verbose_name_plural} were selected for deletion.",
            )
            return redirect(self.get_return_url(request))

        data.update({"table": table})
        return Response(data)

bulk_destroy(request, *args, **kwargs)

Call perform_bulk_destroy(). The function exist to keep the DRF's get/post pattern of {action}/perform_{action}, we will need it when we transition from using forms to serializers in the UI. User should override this function to handle any actions as needed before bulk destroy.

Source code in nautobot/core/views/mixins.py
def bulk_destroy(self, request, *args, **kwargs):
    """
    Call perform_bulk_destroy().
    The function exist to keep the DRF's get/post pattern of {action}/perform_{action}, we will need it when we transition from using forms to serializers in the UI.
    User should override this function to handle any actions as needed before bulk destroy.
    """
    return self.perform_bulk_destroy(request, **kwargs)

perform_bulk_destroy(request, **kwargs)

request.POST "_delete": Function to render the user selection of objects in a table form/BulkDestroyConfirmationForm via Response that is passed to NautobotHTMLRenderer. request.POST "_confirm": Function to validate the table form/BulkDestroyConfirmationForm and to perform the action of bulk destroy. Render the form with errors if exceptions are raised.

Source code in nautobot/core/views/mixins.py
def perform_bulk_destroy(self, request, **kwargs):
    """
    request.POST "_delete": Function to render the user selection of objects in a table form/BulkDestroyConfirmationForm via Response that is passed to NautobotHTMLRenderer.
    request.POST "_confirm": Function to validate the table form/BulkDestroyConfirmationForm and to perform the action of bulk destroy. Render the form with errors if exceptions are raised.
    """
    queryset = self.get_queryset()
    model = queryset.model
    # Are we deleting *all* objects in the queryset or just a selected subset?
    if request.POST.get("_all"):
        filter_params = self.get_filter_params(request)
        if not filter_params:
            self.pk_list = model.objects.only("pk").all().values_list("pk", flat=True)
        elif self.filterset_class is None:
            raise NotImplementedError("filterset_class must be defined to use _all")
        else:
            self.pk_list = self.filterset_class(filter_params, model.objects.only("pk")).qs
    else:
        self.pk_list = request.POST.getlist("pk")
    form_class = self.get_form_class(**kwargs)
    data = {}
    if "_confirm" in request.POST:
        form = form_class(request.POST)
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)
    table_class = self.get_table_class()
    table = table_class(queryset.filter(pk__in=self.pk_list), orderable=False)
    if not table.rows:
        messages.warning(
            request,
            f"No {queryset.model._meta.verbose_name_plural} were selected for deletion.",
        )
        return redirect(self.get_return_url(request))

    data.update({"table": table})
    return Response(data)

nautobot.apps.views.ObjectBulkUpdateViewMixin

Bases: NautobotViewSetMixin, BulkUpdateModelMixin

UI mixin to bulk update model instances.

Source code in nautobot/core/views/mixins.py
class ObjectBulkUpdateViewMixin(NautobotViewSetMixin, BulkUpdateModelMixin):
    """
    UI mixin to bulk update model instances.
    """

    filterset_class = None
    bulk_update_form_class = None

    def _process_bulk_update_form(self, form):
        request = self.request
        queryset = self.get_queryset()
        model = queryset.model
        form_custom_fields = getattr(form, "custom_fields", [])
        form_relationships = getattr(form, "relationships", [])
        # Standard fields are those that are intrinsic to self.model in the form
        # Relationships, custom fields, object_note are extrinsic fields
        # PK is used to identify an existing instance, not to modify the object
        standard_fields = [
            field
            for field in form.fields
            if field not in form_custom_fields + form_relationships + ["pk"] + ["object_note"]
        ]
        nullified_fields = request.POST.getlist("_nullify")
        with transaction.atomic():
            updated_objects = []
            for obj in queryset.filter(pk__in=form.cleaned_data["pk"]):
                self.obj = obj
                # Update standard fields. If a field is listed in _nullify, delete its value.
                for name in standard_fields:
                    try:
                        model_field = model._meta.get_field(name)
                    except FieldDoesNotExist:
                        # This form field is used to modify a field rather than set its value directly
                        model_field = None
                    # Handle nullification
                    if name in form.nullable_fields and name in nullified_fields:
                        if isinstance(model_field, ManyToManyField):
                            getattr(obj, name).set([])
                        else:
                            setattr(obj, name, None if model_field is not None and model_field.null else "")
                    # ManyToManyFields
                    elif isinstance(model_field, ManyToManyField):
                        if form.cleaned_data[name]:
                            getattr(obj, name).set(form.cleaned_data[name])
                    # Normal fields
                    elif form.cleaned_data[name] not in (None, ""):
                        setattr(obj, name, form.cleaned_data[name])
                # Update custom fields
                for field_name in form_custom_fields:
                    if field_name in form.nullable_fields and field_name in nullified_fields:
                        obj.cf[remove_prefix_from_cf_key(field_name)] = None
                    elif form.cleaned_data.get(field_name) not in (None, "", []):
                        obj.cf[remove_prefix_from_cf_key(field_name)] = form.cleaned_data[field_name]

                obj.validated_save()
                updated_objects.append(obj)
                self.logger.debug(f"Saved {obj} (PK: {obj.pk})")

                # Add/remove tags
                if form.cleaned_data.get("add_tags", None):
                    obj.tags.add(*form.cleaned_data["add_tags"])
                if form.cleaned_data.get("remove_tags", None):
                    obj.tags.remove(*form.cleaned_data["remove_tags"])

                if hasattr(form, "save_relationships") and callable(form.save_relationships):
                    # Add/remove relationship associations
                    form.save_relationships(instance=obj, nullified_fields=nullified_fields)

                if hasattr(form, "save_note") and callable(form.save_note):
                    form.save_note(instance=obj, user=request.user)

            # Enforce object-level permissions
            if queryset.filter(pk__in=[obj.pk for obj in updated_objects]).count() != len(updated_objects):
                raise ObjectDoesNotExist
        if updated_objects:
            msg = f"Updated {len(updated_objects)} {model._meta.verbose_name_plural}"
            self.logger.info(msg)
            messages.success(self.request, msg)
        self.success_url = self.get_return_url(request)

    def bulk_update(self, request, *args, **kwargs):
        """
        Call perform_bulk_update().
        The function exist to keep the DRF's get/post pattern of {action}/perform_{action}, we will need it when we transition from using forms to serializers in the UI.
        User should override this function to handle any actions as needed before bulk update.
        """
        return self.perform_bulk_update(request, **kwargs)

    # TODO: this conflicts with BulkUpdateModelMixin.perform_bulk_update(self, objects, update_data, partial)
    def perform_bulk_update(self, request, **kwargs):  # pylint: disable=arguments-differ
        """
        request.POST "_edit": Function to render the user selection of objects in a table form/BulkUpdateForm via Response that is passed to NautobotHTMLRenderer.
        request.POST "_apply": Function to validate the table form/BulkUpdateForm and to perform the action of bulk update. Render the form with errors if exceptions are raised.
        """
        queryset = self.get_queryset()
        model = queryset.model

        # If we are editing *all* objects in the queryset, replace the PK list with all matched objects.
        if request.POST.get("_all"):
            filter_params = self.get_filter_params(request)
            if not filter_params:
                self.pk_list = model.objects.only("pk").all().values_list("pk", flat=True)
            elif self.filterset_class is None:
                raise NotImplementedError("filterset_class must be defined to use _all")
            else:
                self.pk_list = self.filterset_class(filter_params, model.objects.only("pk")).qs
        else:
            self.pk_list = request.POST.getlist("pk")
        data = {}
        form_class = self.get_form_class()
        if "_apply" in request.POST:
            self.kwargs = kwargs
            form = form_class(model, request.POST)
            restrict_form_fields(form, request.user)
            if form.is_valid():
                return self.form_valid(form)
            else:
                return self.form_invalid(form)
        table_class = self.get_table_class()
        table = table_class(queryset.filter(pk__in=self.pk_list), orderable=False)
        if not table.rows:
            messages.warning(
                request,
                f"No {queryset.model._meta.verbose_name_plural} were selected to update.",
            )
            return redirect(self.get_return_url(request))
        data.update({"table": table})
        return Response(data)

bulk_update(request, *args, **kwargs)

Call perform_bulk_update(). The function exist to keep the DRF's get/post pattern of {action}/perform_{action}, we will need it when we transition from using forms to serializers in the UI. User should override this function to handle any actions as needed before bulk update.

Source code in nautobot/core/views/mixins.py
def bulk_update(self, request, *args, **kwargs):
    """
    Call perform_bulk_update().
    The function exist to keep the DRF's get/post pattern of {action}/perform_{action}, we will need it when we transition from using forms to serializers in the UI.
    User should override this function to handle any actions as needed before bulk update.
    """
    return self.perform_bulk_update(request, **kwargs)

perform_bulk_update(request, **kwargs)

request.POST "_edit": Function to render the user selection of objects in a table form/BulkUpdateForm via Response that is passed to NautobotHTMLRenderer. request.POST "_apply": Function to validate the table form/BulkUpdateForm and to perform the action of bulk update. Render the form with errors if exceptions are raised.

Source code in nautobot/core/views/mixins.py
def perform_bulk_update(self, request, **kwargs):  # pylint: disable=arguments-differ
    """
    request.POST "_edit": Function to render the user selection of objects in a table form/BulkUpdateForm via Response that is passed to NautobotHTMLRenderer.
    request.POST "_apply": Function to validate the table form/BulkUpdateForm and to perform the action of bulk update. Render the form with errors if exceptions are raised.
    """
    queryset = self.get_queryset()
    model = queryset.model

    # If we are editing *all* objects in the queryset, replace the PK list with all matched objects.
    if request.POST.get("_all"):
        filter_params = self.get_filter_params(request)
        if not filter_params:
            self.pk_list = model.objects.only("pk").all().values_list("pk", flat=True)
        elif self.filterset_class is None:
            raise NotImplementedError("filterset_class must be defined to use _all")
        else:
            self.pk_list = self.filterset_class(filter_params, model.objects.only("pk")).qs
    else:
        self.pk_list = request.POST.getlist("pk")
    data = {}
    form_class = self.get_form_class()
    if "_apply" in request.POST:
        self.kwargs = kwargs
        form = form_class(model, request.POST)
        restrict_form_fields(form, request.user)
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)
    table_class = self.get_table_class()
    table = table_class(queryset.filter(pk__in=self.pk_list), orderable=False)
    if not table.rows:
        messages.warning(
            request,
            f"No {queryset.model._meta.verbose_name_plural} were selected to update.",
        )
        return redirect(self.get_return_url(request))
    data.update({"table": table})
    return Response(data)

nautobot.apps.views.ObjectChangeLogViewMixin

Bases: NautobotViewSetMixin

UI mixin to list a model's changelog queryset

Source code in nautobot/core/views/mixins.py
class ObjectChangeLogViewMixin(NautobotViewSetMixin):
    """
    UI mixin to list a model's changelog queryset
    """

    base_template = None

    @drf_action(detail=True)
    def changelog(self, request, *args, **kwargs):
        data = {
            "base_template": self.base_template,
        }
        return Response(data)

nautobot.apps.views.ObjectDeleteView

Bases: GetReturnURLMixin, ObjectPermissionRequiredMixin, View

Delete a single object.

queryset: The base queryset for the object being deleted template_name: The name of the template

Source code in nautobot/core/views/generic.py
class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
    """
    Delete a single object.

    queryset: The base queryset for the object being deleted
    template_name: The name of the template
    """

    queryset = None
    template_name = "generic/object_delete.html"

    def get_required_permission(self):
        return get_permission_for_model(self.queryset.model, "delete")

    def get_object(self, kwargs):
        """Retrieve an object based on `kwargs`."""
        # Look up an existing object by PK, name, or slug, if provided.
        for field in ("pk", "name", "slug"):
            if field in kwargs:
                return get_object_or_404(self.queryset, **{field: kwargs[field]})
        return self.queryset.model()

    def get(self, request, **kwargs):
        obj = self.get_object(kwargs)
        form = ConfirmationForm(initial=request.GET)

        return render(
            request,
            self.template_name,
            {
                "obj": obj,
                "form": form,
                "obj_type": self.queryset.model._meta.verbose_name,
                "return_url": self.get_return_url(request, obj),
            },
        )

    def post(self, request, **kwargs):
        logger = logging.getLogger(__name__ + ".ObjectDeleteView")
        obj = self.get_object(kwargs)
        form = ConfirmationForm(request.POST)

        if form.is_valid():
            logger.debug("Form validation was successful")

            try:
                obj.delete()
            except ProtectedError as e:
                logger.info("Caught ProtectedError while attempting to delete object")
                handle_protectederror([obj], request, e)
                return redirect(obj.get_absolute_url())

            msg = f"Deleted {self.queryset.model._meta.verbose_name} {obj}"
            logger.info(msg)
            messages.success(request, msg)

            return_url = form.cleaned_data.get("return_url")
            if url_has_allowed_host_and_scheme(url=return_url, allowed_hosts=request.get_host()):
                return redirect(iri_to_uri(return_url))
            else:
                return redirect(self.get_return_url(request, obj))

        else:
            logger.debug("Form validation failed")

        return render(
            request,
            self.template_name,
            {
                "obj": obj,
                "form": form,
                "obj_type": self.queryset.model._meta.verbose_name,
                "return_url": self.get_return_url(request, obj),
            },
        )

get_object(kwargs)

Retrieve an object based on kwargs.

Source code in nautobot/core/views/generic.py
def get_object(self, kwargs):
    """Retrieve an object based on `kwargs`."""
    # Look up an existing object by PK, name, or slug, if provided.
    for field in ("pk", "name", "slug"):
        if field in kwargs:
            return get_object_or_404(self.queryset, **{field: kwargs[field]})
    return self.queryset.model()

nautobot.apps.views.ObjectDestroyViewMixin

Bases: NautobotViewSetMixin, mixins.DestroyModelMixin

UI mixin to destroy a model instance.

Source code in nautobot/core/views/mixins.py
class ObjectDestroyViewMixin(NautobotViewSetMixin, mixins.DestroyModelMixin):
    """
    UI mixin to destroy a model instance.
    """

    destroy_form_class = ConfirmationForm

    def _process_destroy_form(self, form):
        request = self.request
        obj = self.obj
        queryset = self.get_queryset()
        try:
            with transaction.atomic():
                obj.delete()
                msg = f"Deleted {queryset.model._meta.verbose_name} {obj}"
                self.logger.info(msg)
                messages.success(request, msg)
                self.success_url = self.get_return_url(request, obj)
        except ProtectedError as e:
            self.logger.info("Caught ProtectedError while attempting to delete object")
            handle_protectederror([obj], request, e)
            self.success_url = obj.get_absolute_url()

    def destroy(self, request, *args, **kwargs):
        """
        request.GET: render the ObjectDeleteConfirmationForm which is passed to NautobotHTMLRenderer as Response.
        request.POST: call perform_destroy() which validates the form and perform the action of delete.
        Override to add more variables to Response
        """
        context = {}
        if request.method == "POST":
            return self.perform_destroy(request, **kwargs)
        return Response(context)

    def perform_destroy(self, request, **kwargs):
        """
        Function to validate the ObjectDeleteConfirmationForm and to delete the object.
        """
        self.obj = self.get_object()
        form_class = self.get_form_class()
        form = form_class(request.POST)
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

destroy(request, *args, **kwargs)

request.GET: render the ObjectDeleteConfirmationForm which is passed to NautobotHTMLRenderer as Response. request.POST: call perform_destroy() which validates the form and perform the action of delete. Override to add more variables to Response

Source code in nautobot/core/views/mixins.py
def destroy(self, request, *args, **kwargs):
    """
    request.GET: render the ObjectDeleteConfirmationForm which is passed to NautobotHTMLRenderer as Response.
    request.POST: call perform_destroy() which validates the form and perform the action of delete.
    Override to add more variables to Response
    """
    context = {}
    if request.method == "POST":
        return self.perform_destroy(request, **kwargs)
    return Response(context)

perform_destroy(request, **kwargs)

Function to validate the ObjectDeleteConfirmationForm and to delete the object.

Source code in nautobot/core/views/mixins.py
def perform_destroy(self, request, **kwargs):
    """
    Function to validate the ObjectDeleteConfirmationForm and to delete the object.
    """
    self.obj = self.get_object()
    form_class = self.get_form_class()
    form = form_class(request.POST)
    if form.is_valid():
        return self.form_valid(form)
    else:
        return self.form_invalid(form)

nautobot.apps.views.ObjectDetailViewMixin

Bases: NautobotViewSetMixin, mixins.RetrieveModelMixin

UI mixin to retrieve a model instance.

Source code in nautobot/core/views/mixins.py
class ObjectDetailViewMixin(NautobotViewSetMixin, mixins.RetrieveModelMixin):
    """
    UI mixin to retrieve a model instance.
    """

    def retrieve(self, request, *args, **kwargs):
        """
        Retrieve a model instance.
        """
        instance = self.get_object()
        serializer = self.get_serializer(instance)

        context = serializer.data
        context["use_new_ui"] = True
        return Response(context)

retrieve(request, *args, **kwargs)

Retrieve a model instance.

Source code in nautobot/core/views/mixins.py
def retrieve(self, request, *args, **kwargs):
    """
    Retrieve a model instance.
    """
    instance = self.get_object()
    serializer = self.get_serializer(instance)

    context = serializer.data
    context["use_new_ui"] = True
    return Response(context)

nautobot.apps.views.ObjectDynamicGroupsView

Bases: View

Present a list of dynamic groups associated to a particular object. base_template: The name of the template to extend. If not provided, "/.html" will be used.

Source code in nautobot/extras/views.py
class ObjectDynamicGroupsView(View):
    """
    Present a list of dynamic groups associated to a particular object.
    base_template: The name of the template to extend. If not provided, "<app>/<model>.html" will be used.
    """

    base_template = None

    def get(self, request, model, **kwargs):
        # Handle QuerySet restriction of parent object if needed
        if hasattr(model.objects, "restrict"):
            obj = get_object_or_404(model.objects.restrict(request.user, "view"), **kwargs)
        else:
            obj = get_object_or_404(model, **kwargs)

        # Gather all dynamic groups for this object (and its related objects)
        dynamicsgroups_table = tables.DynamicGroupTable(data=obj.dynamic_groups_cached, orderable=False)

        # Apply the request context
        paginate = {
            "paginator_class": EnhancedPaginator,
            "per_page": get_paginate_count(request),
        }
        RequestConfig(request, paginate).configure(dynamicsgroups_table)

        self.base_template = get_base_template(self.base_template, model)

        return render(
            request,
            "extras/object_dynamicgroups.html",
            {
                "object": obj,
                "verbose_name": obj._meta.verbose_name,
                "verbose_name_plural": obj._meta.verbose_name_plural,
                "table": dynamicsgroups_table,
                "base_template": self.base_template,
                "active_tab": "dynamic-groups",
            },
        )

nautobot.apps.views.ObjectEditView

Bases: GetReturnURLMixin, ObjectPermissionRequiredMixin, View

Create or edit a single object.

queryset: The base queryset for the object being modified model_form: The form used to create or edit the object template_name: The name of the template

Source code in nautobot/core/views/generic.py
class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
    """
    Create or edit a single object.

    queryset: The base queryset for the object being modified
    model_form: The form used to create or edit the object
    template_name: The name of the template
    """

    queryset = None
    model_form = None
    template_name = "generic/object_edit.html"

    def get_required_permission(self):
        # self._permission_action is set by dispatch() to either "add" or "change" depending on whether
        # we are modifying an existing object or creating a new one.
        return get_permission_for_model(self.queryset.model, self._permission_action)

    def get_object(self, kwargs):
        """Retrieve an object based on `kwargs`."""
        # Look up an existing object by PK, name, or slug, if provided.
        for field in ("pk", "name", "slug"):
            if field in kwargs:
                return get_object_or_404(self.queryset, **{field: kwargs[field]})
        return self.queryset.model()

    def get_extra_context(self, request, instance):
        """
        Return any additional context data for the template.

        Args:
            request (HttpRequest): The current request
            instance (Model): The object being edited

        Returns:
            (dict): Additional context data
        """
        return {}

    def alter_obj(self, obj, request, url_args, url_kwargs):
        # Allow views to add extra info to an object before it is processed. For example, a parent object can be defined
        # given some parameter from the request URL.
        return obj

    def dispatch(self, request, *args, **kwargs):
        # Determine required permission based on whether we are editing an existing object
        self._permission_action = "change" if kwargs else "add"

        return super().dispatch(request, *args, **kwargs)

    def get(self, request, *args, **kwargs):
        obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)

        initial_data = normalize_querydict(request.GET, form_class=self.model_form)
        form = self.model_form(instance=obj, initial=initial_data)
        restrict_form_fields(form, request.user)

        return render(
            request,
            self.template_name,
            {
                "obj": obj,
                "obj_type": self.queryset.model._meta.verbose_name,
                "form": form,
                "return_url": self.get_return_url(request, obj),
                "editing": obj.present_in_database,
                **self.get_extra_context(request, obj),
            },
        )

    def successful_post(self, request, obj, created, logger):
        """Callback after the form is successfully saved but before redirecting the user."""
        verb = "Created" if created else "Modified"
        msg = f"{verb} {self.queryset.model._meta.verbose_name}"
        logger.info(f"{msg} {obj} (PK: {obj.pk})")
        try:
            msg = format_html('{} <a href="{}">{}</a>', msg, obj.get_absolute_url(), obj)
        except AttributeError:
            msg = format_html("{} {}", msg, obj)
        messages.success(request, msg)

    def post(self, request, *args, **kwargs):
        logger = logging.getLogger(__name__ + ".ObjectEditView")
        obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
        form = self.model_form(data=request.POST, files=request.FILES, instance=obj)
        restrict_form_fields(form, request.user)

        if form.is_valid():
            logger.debug("Form validation was successful")

            try:
                with transaction.atomic():
                    object_created = not form.instance.present_in_database
                    obj = form.save()

                    # Check that the new object conforms with any assigned object-level permissions
                    self.queryset.get(pk=obj.pk)

                if hasattr(form, "save_note") and callable(form.save_note):
                    form.save_note(instance=obj, user=request.user)

                self.successful_post(request, obj, object_created, logger)

                if "_addanother" in request.POST:
                    # If the object has clone_fields, pre-populate a new instance of the form
                    if hasattr(obj, "clone_fields"):
                        url = f"{request.path}?{prepare_cloned_fields(obj)}"
                        return redirect(url)

                    return redirect(request.get_full_path())

                return_url = form.cleaned_data.get("return_url")
                if url_has_allowed_host_and_scheme(url=return_url, allowed_hosts=request.get_host()):
                    return redirect(iri_to_uri(return_url))
                else:
                    return redirect(self.get_return_url(request, obj))

            except ObjectDoesNotExist:
                msg = "Object save failed due to object-level permissions violation"
                logger.debug(msg)
                form.add_error(None, msg)

        else:
            logger.debug("Form validation failed")

        return render(
            request,
            self.template_name,
            {
                "obj": obj,
                "obj_type": self.queryset.model._meta.verbose_name,
                "form": form,
                "return_url": self.get_return_url(request, obj),
                "editing": obj.present_in_database,
                **self.get_extra_context(request, obj),
            },
        )

get_extra_context(request, instance)

Return any additional context data for the template.

Parameters:

Name Type Description Default
request HttpRequest

The current request

required
instance Model

The object being edited

required

Returns:

Type Description
dict

Additional context data

Source code in nautobot/core/views/generic.py
def get_extra_context(self, request, instance):
    """
    Return any additional context data for the template.

    Args:
        request (HttpRequest): The current request
        instance (Model): The object being edited

    Returns:
        (dict): Additional context data
    """
    return {}

get_object(kwargs)

Retrieve an object based on kwargs.

Source code in nautobot/core/views/generic.py
def get_object(self, kwargs):
    """Retrieve an object based on `kwargs`."""
    # Look up an existing object by PK, name, or slug, if provided.
    for field in ("pk", "name", "slug"):
        if field in kwargs:
            return get_object_or_404(self.queryset, **{field: kwargs[field]})
    return self.queryset.model()

successful_post(request, obj, created, logger)

Callback after the form is successfully saved but before redirecting the user.

Source code in nautobot/core/views/generic.py
def successful_post(self, request, obj, created, logger):
    """Callback after the form is successfully saved but before redirecting the user."""
    verb = "Created" if created else "Modified"
    msg = f"{verb} {self.queryset.model._meta.verbose_name}"
    logger.info(f"{msg} {obj} (PK: {obj.pk})")
    try:
        msg = format_html('{} <a href="{}">{}</a>', msg, obj.get_absolute_url(), obj)
    except AttributeError:
        msg = format_html("{} {}", msg, obj)
    messages.success(request, msg)

nautobot.apps.views.ObjectEditViewMixin

Bases: NautobotViewSetMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin

UI mixin to create or update a model instance.

Source code in nautobot/core/views/mixins.py
class ObjectEditViewMixin(NautobotViewSetMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin):
    """
    UI mixin to create or update a model instance.
    """

    def _process_create_or_update_form(self, form):
        """
        Helper method to create or update an object after the form is validated successfully.
        """
        request = self.request
        queryset = self.get_queryset()
        with transaction.atomic():
            object_created = not form.instance.present_in_database
            obj = self.form_save(form)

            # Check that the new object conforms with any assigned object-level permissions
            queryset.get(pk=obj.pk)

            if hasattr(form, "save_note") and callable(form.save_note):
                form.save_note(instance=obj, user=request.user)

            msg = f'{"Created" if object_created else "Modified"} {queryset.model._meta.verbose_name}'
            self.logger.info(f"{msg} {obj} (PK: {obj.pk})")
            try:
                msg = format_html('{} <a href="{}">{}</a>', msg, obj.get_absolute_url(), obj)
            except AttributeError:
                msg = format_html("{} {}", msg, obj)
            messages.success(request, msg)
            if "_addanother" in request.POST:
                # If the object has clone_fields, pre-populate a new instance of the form
                if hasattr(obj, "clone_fields"):
                    url = f"{request.path}?{prepare_cloned_fields(obj)}"
                    self.success_url = url
                self.success_url = request.get_full_path()
            else:
                return_url = form.cleaned_data.get("return_url")
                if url_has_allowed_host_and_scheme(url=return_url, allowed_hosts=request.get_host()):
                    self.success_url = iri_to_uri(return_url)
                else:
                    self.success_url = self.get_return_url(request, obj)

    def create(self, request, *args, **kwargs):
        """
        request.GET: render the ObjectForm which is passed to NautobotHTMLRenderer as Response.
        request.POST: call perform_create() which validates the form and perform the action of create.
        Override to add more variables to Response.
        """
        context = {}
        if request.method == "POST":
            return self.perform_create(request, *args, **kwargs)
        return Response(context)

    # TODO: this conflicts with DRF's CreateModelMixin.perform_create(self, serializer) API
    def perform_create(self, request, *args, **kwargs):  # pylint: disable=arguments-differ
        """
        Function to validate the ObjectForm and to create a new object.
        """
        self.obj = self.get_object()
        form_class = self.get_form_class()
        form = form_class(data=request.POST, files=request.FILES, instance=self.obj)
        restrict_form_fields(form, request.user)
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    def update(self, request, *args, **kwargs):
        """
        request.GET: render the ObjectEditForm which is passed to NautobotHTMLRenderer as Response.
        request.POST: call perform_update() which validates the form and perform the action of update/partial_update of an existing object.
        Override to add more variables to Response.
        """
        context = {}
        if request.method == "POST":
            return self.perform_update(request, *args, **kwargs)
        return Response(context)

    # TODO: this conflicts with DRF's UpdateModelMixin.perform_update(self, serializer) API
    def perform_update(self, request, *args, **kwargs):  # pylint: disable=arguments-differ
        """
        Function to validate the ObjectEditForm and to update/partial_update an existing object.
        """
        self.obj = self.get_object()
        form_class = self.get_form_class()
        form = form_class(data=request.POST, files=request.FILES, instance=self.obj)
        restrict_form_fields(form, request.user)
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

create(request, *args, **kwargs)

request.GET: render the ObjectForm which is passed to NautobotHTMLRenderer as Response. request.POST: call perform_create() which validates the form and perform the action of create. Override to add more variables to Response.

Source code in nautobot/core/views/mixins.py
def create(self, request, *args, **kwargs):
    """
    request.GET: render the ObjectForm which is passed to NautobotHTMLRenderer as Response.
    request.POST: call perform_create() which validates the form and perform the action of create.
    Override to add more variables to Response.
    """
    context = {}
    if request.method == "POST":
        return self.perform_create(request, *args, **kwargs)
    return Response(context)

perform_create(request, *args, **kwargs)

Function to validate the ObjectForm and to create a new object.

Source code in nautobot/core/views/mixins.py
def perform_create(self, request, *args, **kwargs):  # pylint: disable=arguments-differ
    """
    Function to validate the ObjectForm and to create a new object.
    """
    self.obj = self.get_object()
    form_class = self.get_form_class()
    form = form_class(data=request.POST, files=request.FILES, instance=self.obj)
    restrict_form_fields(form, request.user)
    if form.is_valid():
        return self.form_valid(form)
    else:
        return self.form_invalid(form)

perform_update(request, *args, **kwargs)

Function to validate the ObjectEditForm and to update/partial_update an existing object.

Source code in nautobot/core/views/mixins.py
def perform_update(self, request, *args, **kwargs):  # pylint: disable=arguments-differ
    """
    Function to validate the ObjectEditForm and to update/partial_update an existing object.
    """
    self.obj = self.get_object()
    form_class = self.get_form_class()
    form = form_class(data=request.POST, files=request.FILES, instance=self.obj)
    restrict_form_fields(form, request.user)
    if form.is_valid():
        return self.form_valid(form)
    else:
        return self.form_invalid(form)

update(request, *args, **kwargs)

request.GET: render the ObjectEditForm which is passed to NautobotHTMLRenderer as Response. request.POST: call perform_update() which validates the form and perform the action of update/partial_update of an existing object. Override to add more variables to Response.

Source code in nautobot/core/views/mixins.py
def update(self, request, *args, **kwargs):
    """
    request.GET: render the ObjectEditForm which is passed to NautobotHTMLRenderer as Response.
    request.POST: call perform_update() which validates the form and perform the action of update/partial_update of an existing object.
    Override to add more variables to Response.
    """
    context = {}
    if request.method == "POST":
        return self.perform_update(request, *args, **kwargs)
    return Response(context)

nautobot.apps.views.ObjectImportView

Bases: GetReturnURLMixin, ObjectPermissionRequiredMixin, View

Import a single object (YAML or JSON format).

queryset: Base queryset for the objects being created model_form: The ModelForm used to create individual objects related_object_forms: A dictionary mapping of forms to be used for the creation of related (child) objects template_name: The name of the template

Source code in nautobot/core/views/generic.py
class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
    """
    Import a single object (YAML or JSON format).

    queryset: Base queryset for the objects being created
    model_form: The ModelForm used to create individual objects
    related_object_forms: A dictionary mapping of forms to be used for the creation of related (child) objects
    template_name: The name of the template
    """

    queryset = None
    model_form = None
    related_object_forms = {}
    template_name = "generic/object_import.html"

    def get_required_permission(self):
        return get_permission_for_model(self.queryset.model, "add")

    def get(self, request):
        form = ImportForm()

        return render(
            request,
            self.template_name,
            {
                "form": form,
                "obj_type": self.queryset.model._meta.verbose_name,
                "return_url": self.get_return_url(request),
            },
        )

    def post(self, request):
        logger = logging.getLogger(__name__ + ".ObjectImportView")
        form = ImportForm(request.POST)

        if form.is_valid():
            logger.debug("Import form validation was successful")

            # Initialize model form
            data = form.cleaned_data["data"]
            model_form = self.model_form(data)
            restrict_form_fields(model_form, request.user)

            # Assign default values for any fields which were not specified. We have to do this manually because passing
            # 'initial=' to the form on initialization merely sets default values for the widgets. Since widgets are not
            # used for YAML/JSON import, we first bind the imported data normally, then update the form's data with the
            # applicable field defaults as needed prior to form validation.
            for field_name, field in model_form.fields.items():
                if field_name not in data and hasattr(field, "initial"):
                    model_form.data[field_name] = field.initial

            if model_form.is_valid():
                try:
                    with transaction.atomic():
                        # Save the primary object
                        obj = model_form.save()

                        # Enforce object-level permissions
                        self.queryset.get(pk=obj.pk)

                        logger.debug(f"Created {obj} (PK: {obj.pk})")

                        # Iterate through the related object forms (if any), validating and saving each instance.
                        for (
                            field_name,
                            related_object_form,
                        ) in self.related_object_forms.items():
                            logger.debug("Processing form for related objects: {related_object_form}")

                            related_obj_pks = []
                            for i, rel_obj_data in enumerate(data.get(field_name, [])):
                                f = related_object_form(obj, rel_obj_data)

                                for subfield_name, field in f.fields.items():
                                    if subfield_name not in rel_obj_data and hasattr(field, "initial"):
                                        f.data[subfield_name] = field.initial

                                if f.is_valid():
                                    related_obj = f.save()
                                    related_obj_pks.append(related_obj.pk)
                                else:
                                    # Replicate errors on the related object form to the primary form for display
                                    for subfield_name, errors in f.errors.items():
                                        for err in errors:
                                            err_msg = f"{field_name}[{i}] {subfield_name}: {err}"
                                            model_form.add_error(None, err_msg)
                                    raise AbortTransaction()

                            # Enforce object-level permissions on related objects
                            model = related_object_form.Meta.model
                            if model.objects.filter(pk__in=related_obj_pks).count() != len(related_obj_pks):
                                raise ObjectDoesNotExist

                except AbortTransaction:
                    pass

                except ObjectDoesNotExist:
                    msg = "Object creation failed due to object-level permissions violation"
                    logger.debug(msg)
                    form.add_error(None, msg)

            if not model_form.errors:
                logger.info(f"Import object {obj} (PK: {obj.pk})")
                messages.success(
                    request,
                    format_html('Imported object: <a href="{}">{}</a>', obj.get_absolute_url(), obj),
                )

                if "_addanother" in request.POST:
                    return redirect(request.get_full_path())

                return_url = form.cleaned_data.get("return_url")
                if url_has_allowed_host_and_scheme(url=return_url, allowed_hosts=request.get_host()):
                    return redirect(iri_to_uri(return_url))
                else:
                    return redirect(self.get_return_url(request, obj))

            else:
                logger.debug("Model form validation failed")

                # Replicate model form errors for display
                for field, errors in model_form.errors.items():
                    for err in errors:
                        if field == "__all__":
                            form.add_error(None, err)
                        else:
                            form.add_error(None, f"{field}: {err}")

        else:
            logger.debug("Import form validation failed")

        return render(
            request,
            self.template_name,
            {
                "form": form,
                "obj_type": self.queryset.model._meta.verbose_name,
                "return_url": self.get_return_url(request),
            },
        )

nautobot.apps.views.ObjectListView

Bases: ObjectPermissionRequiredMixin, View

List a series of objects.

The queryset of objects to display. Note: Prefetching related objects is not necessary, as the

table will prefetch objects as needed depending on the columns being displayed.

filter: A django-filter FilterSet that is applied to the queryset filter_form: The form used to render filter options table: The django-tables2 Table used to render the objects list template_name: The name of the template non_filter_params: List of query parameters that are not used for queryset filtering

Source code in nautobot/core/views/generic.py
class ObjectListView(ObjectPermissionRequiredMixin, View):
    """
    List a series of objects.

    queryset: The queryset of objects to display. Note: Prefetching related objects is not necessary, as the
      table will prefetch objects as needed depending on the columns being displayed.
    filter: A django-filter FilterSet that is applied to the queryset
    filter_form: The form used to render filter options
    table: The django-tables2 Table used to render the objects list
    template_name: The name of the template
    non_filter_params: List of query parameters that are **not** used for queryset filtering
    """

    queryset = None
    filterset = None
    filterset_form = None
    table = None
    template_name = "generic/object_list.html"
    action_buttons = ("add", "import", "export")
    non_filter_params = (
        "export",  # trigger for CSV/export-template/YAML export # 3.0 TODO: remove, irrelevant after #4746
        "page",  # used by django-tables2.RequestConfig
        "per_page",  # used by get_paginate_count
        "sort",  # table sorting
    )

    def get_filter_params(self, request):
        """Helper function - take request.GET and discard any parameters that are not used for queryset filtering."""
        filter_params = request.GET.copy()
        return get_filterable_params_from_filter_params(filter_params, self.non_filter_params, self.filterset())

    def get_required_permission(self):
        return get_permission_for_model(self.queryset.model, "view")

    # 3.0 TODO: remove, irrelevant after #4746
    def queryset_to_yaml(self):
        """
        Export the queryset of objects as concatenated YAML documents.
        """
        yaml_data = [obj.to_yaml() for obj in self.queryset]

        return "---\n".join(yaml_data)

    def validate_action_buttons(self, request):
        """Verify actions in self.action_buttons are valid view actions."""

        always_valid_actions = ("export",)
        valid_actions = []
        invalid_actions = []
        # added check for whether the action_buttons exist because of issue #2107
        if self.action_buttons is None:
            self.action_buttons = []
        for action in self.action_buttons:
            if action in always_valid_actions or validated_viewname(self.queryset.model, action) is not None:
                valid_actions.append(action)
            else:
                invalid_actions.append(action)
        if invalid_actions:
            messages.error(request, f"Missing views for action(s) {', '.join(invalid_actions)}")
        return valid_actions

    def get(self, request):
        model = self.queryset.model
        content_type = ContentType.objects.get_for_model(model)

        display_filter_params = []
        dynamic_filter_form = None
        filter_form = None

        if self.filterset:
            filter_params = self.get_filter_params(request)
            filterset = self.filterset(filter_params, self.queryset)
            self.queryset = filterset.qs
            if not filterset.is_valid():
                messages.error(
                    request,
                    format_html("Invalid filters were specified: {}", filterset.errors),
                )
                self.queryset = self.queryset.none()

            display_filter_params = [
                check_filter_for_display(filterset.filters, field_name, values)
                for field_name, values in filter_params.items()
            ]

            if request.GET:
                factory_formset_params = convert_querydict_to_factory_formset_acceptable_querydict(
                    request.GET, filterset
                )
                dynamic_filter_form = DynamicFilterFormSet(filterset=filterset, data=factory_formset_params)
            else:
                dynamic_filter_form = DynamicFilterFormSet(filterset=filterset)

            if self.filterset_form:
                filter_form = self.filterset_form(filter_params, label_suffix="")

        # Check for export template rendering
        if request.GET.get("export"):  # 3.0 TODO: remove, irrelevant after #4746
            et = get_object_or_404(
                ExportTemplate,
                content_type=content_type,
                name=request.GET.get("export"),
            )
            try:
                return et.render_to_response(self.queryset)
            except Exception as e:
                messages.error(
                    request,
                    f"There was an error rendering the selected export template ({et.name}): {e}",
                )

        # Check for YAML export support
        elif "export" in request.GET and hasattr(model, "to_yaml"):  # 3.0 TODO: remove, irrelevant after #4746
            response = HttpResponse(self.queryset_to_yaml(), content_type="text/yaml")
            filename = f"{settings.BRANDING_PREPENDED_FILENAME}{self.queryset.model._meta.verbose_name_plural}.yaml"
            response["Content-Disposition"] = f'attachment; filename="{filename}"'
            return response

        # Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list
        self.queryset = self.alter_queryset(request)

        # Compile a dictionary indicating which permissions are available to the current user for this model
        permissions = {}
        for action in ("add", "change", "delete", "view"):
            perm_name = get_permission_for_model(model, action)
            permissions[action] = request.user.has_perm(perm_name)

        table = None
        table_config_form = None
        if self.table:
            # Construct the objects table
            # Order By is needed in the table `__init__` method
            order_by = self.request.GET.getlist("sort")
            table = self.table(self.queryset, user=request.user, order_by=order_by)
            if "pk" in table.base_columns and (permissions["change"] or permissions["delete"]):
                table.columns.show("pk")

            # Apply the request context
            paginate = {
                "paginator_class": EnhancedPaginator,
                "per_page": get_paginate_count(request),
            }
            RequestConfig(request, paginate).configure(table)
            table_config_form = TableConfigForm(table=table)
            max_page_size = get_settings_or_config("MAX_PAGE_SIZE")
            if max_page_size and paginate["per_page"] > max_page_size:
                messages.warning(
                    request,
                    f'Requested "per_page" is too large. No more than {max_page_size} items may be displayed at a time.',
                )

        # For the search form field, use a custom placeholder.
        q_placeholder = "Search " + bettertitle(model._meta.verbose_name_plural)
        search_form = SearchForm(data=request.GET, q_placeholder=q_placeholder)

        valid_actions = self.validate_action_buttons(request)

        context = {
            "content_type": content_type,
            "table": table,
            "permissions": permissions,
            "action_buttons": valid_actions,
            "table_config_form": table_config_form,
            "filter_params": display_filter_params,
            "filter_form": filter_form,
            "dynamic_filter_form": dynamic_filter_form,
            "search_form": search_form,
            "list_url": validated_viewname(model, "list"),
            "title": bettertitle(model._meta.verbose_name_plural),
        }

        # `extra_context()` would require `request` access, however `request` parameter cannot simply be
        # added to `extra_context()` because  this method has been used by multiple apps without any parameters.
        # Changing 'def extra context()' to 'def extra context(request)' might break current methods
        # in plugins and core that either override or implement it without request.
        setattr(self, "request", request)
        context.update(self.extra_context())

        return render(request, self.template_name, context)

    def alter_queryset(self, request):
        # .all() is necessary to avoid caching queries
        return self.queryset.all()

    def extra_context(self):
        return {}

get_filter_params(request)

Helper function - take request.GET and discard any parameters that are not used for queryset filtering.

Source code in nautobot/core/views/generic.py
def get_filter_params(self, request):
    """Helper function - take request.GET and discard any parameters that are not used for queryset filtering."""
    filter_params = request.GET.copy()
    return get_filterable_params_from_filter_params(filter_params, self.non_filter_params, self.filterset())

queryset_to_yaml()

Export the queryset of objects as concatenated YAML documents.

Source code in nautobot/core/views/generic.py
def queryset_to_yaml(self):
    """
    Export the queryset of objects as concatenated YAML documents.
    """
    yaml_data = [obj.to_yaml() for obj in self.queryset]

    return "---\n".join(yaml_data)

validate_action_buttons(request)

Verify actions in self.action_buttons are valid view actions.

Source code in nautobot/core/views/generic.py
def validate_action_buttons(self, request):
    """Verify actions in self.action_buttons are valid view actions."""

    always_valid_actions = ("export",)
    valid_actions = []
    invalid_actions = []
    # added check for whether the action_buttons exist because of issue #2107
    if self.action_buttons is None:
        self.action_buttons = []
    for action in self.action_buttons:
        if action in always_valid_actions or validated_viewname(self.queryset.model, action) is not None:
            valid_actions.append(action)
        else:
            invalid_actions.append(action)
    if invalid_actions:
        messages.error(request, f"Missing views for action(s) {', '.join(invalid_actions)}")
    return valid_actions

nautobot.apps.views.ObjectListViewMixin

Bases: NautobotViewSetMixin, mixins.ListModelMixin

UI mixin to list a model queryset

Source code in nautobot/core/views/mixins.py
class ObjectListViewMixin(NautobotViewSetMixin, mixins.ListModelMixin):
    """
    UI mixin to list a model queryset
    """

    action_buttons = ("add", "import", "export")
    filterset_class = None
    filterset_form_class = None
    non_filter_params = (
        "export",  # trigger for CSV/export-template/YAML export # 3.0 TODO: remove, irrelevant after #4746
        "page",  # used by django-tables2.RequestConfig
        "per_page",  # used by get_paginate_count
        "sort",  # table sorting
    )

    def filter_queryset(self, queryset):
        """
        Filter a query with request querystrings.
        """
        if self.filterset_class is not None:
            self.filter_params = self.get_filter_params(self.request)
            self.filterset = self.filterset_class(self.filter_params, queryset)
            queryset = self.filterset.qs
            if not self.filterset.is_valid():
                messages.error(
                    self.request,
                    format_html("Invalid filters were specified: {}", self.filterset.errors),
                )
                queryset = queryset.none()
        return queryset

    # 3.0 TODO: remove, irrelevant after #4746
    def check_for_export(self, request, model, content_type):
        # Check for export template rendering
        queryset = self.filter_queryset(self.get_queryset())
        if request.GET.get("export"):
            et = get_object_or_404(
                ExportTemplate,
                content_type=content_type,
                name=request.GET.get("export"),
            )
            try:
                return et.render_to_response(queryset)
            except Exception as e:
                messages.error(
                    request,
                    f"There was an error rendering the selected export template ({et.name}): {e}",
                )

        # Check for YAML export support
        elif "export" in request.GET and hasattr(model, "to_yaml"):
            response = HttpResponse(self.queryset_to_yaml(), content_type="text/yaml")
            filename = f"nautobot_{queryset.model._meta.verbose_name_plural}.yaml"
            response["Content-Disposition"] = f'attachment; filename="{filename}"'
            return response

        return None

    # 3.0 TODO: remove, irrelevant after #4746
    def queryset_to_yaml(self):
        """
        Export the queryset of objects as concatenated YAML documents.
        """
        queryset = self.filter_queryset(self.get_queryset())
        yaml_data = [obj.to_yaml() for obj in queryset]

        return "---\n".join(yaml_data)

    def list(self, request, *args, **kwargs):
        """
        List the model instances.
        """
        context = {"use_new_ui": True}
        if "export" in request.GET:  # 3.0 TODO: remove, irrelevant after #4746
            queryset = self.get_queryset()
            model = queryset.model
            content_type = ContentType.objects.get_for_model(model)
            response = self.check_for_export(request, model, content_type)
            if response is not None:
                return response
        return Response(context)

filter_queryset(queryset)

Filter a query with request querystrings.

Source code in nautobot/core/views/mixins.py
def filter_queryset(self, queryset):
    """
    Filter a query with request querystrings.
    """
    if self.filterset_class is not None:
        self.filter_params = self.get_filter_params(self.request)
        self.filterset = self.filterset_class(self.filter_params, queryset)
        queryset = self.filterset.qs
        if not self.filterset.is_valid():
            messages.error(
                self.request,
                format_html("Invalid filters were specified: {}", self.filterset.errors),
            )
            queryset = queryset.none()
    return queryset

list(request, *args, **kwargs)

List the model instances.

Source code in nautobot/core/views/mixins.py
def list(self, request, *args, **kwargs):
    """
    List the model instances.
    """
    context = {"use_new_ui": True}
    if "export" in request.GET:  # 3.0 TODO: remove, irrelevant after #4746
        queryset = self.get_queryset()
        model = queryset.model
        content_type = ContentType.objects.get_for_model(model)
        response = self.check_for_export(request, model, content_type)
        if response is not None:
            return response
    return Response(context)

queryset_to_yaml()

Export the queryset of objects as concatenated YAML documents.

Source code in nautobot/core/views/mixins.py
def queryset_to_yaml(self):
    """
    Export the queryset of objects as concatenated YAML documents.
    """
    queryset = self.filter_queryset(self.get_queryset())
    yaml_data = [obj.to_yaml() for obj in queryset]

    return "---\n".join(yaml_data)

nautobot.apps.views.ObjectNotesView

Bases: View

Present a list of notes associated to a particular object. base_template: The name of the template to extend. If not provided, "/.html" will be used.

Source code in nautobot/extras/views.py
class ObjectNotesView(View):
    """
    Present a list of notes associated to a particular object.
    base_template: The name of the template to extend. If not provided, "<app>/<model>.html" will be used.
    """

    base_template = None

    def get(self, request, model, **kwargs):
        # Handle QuerySet restriction of parent object if needed
        if hasattr(model.objects, "restrict"):
            obj = get_object_or_404(model.objects.restrict(request.user, "view"), **kwargs)
        else:
            obj = get_object_or_404(model, **kwargs)

        notes_form = forms.NoteForm(
            initial={
                "assigned_object_type": ContentType.objects.get_for_model(obj),
                "assigned_object_id": obj.pk,
            }
        )
        notes_table = tables.NoteTable(obj.notes)

        # Apply the request context
        paginate = {
            "paginator_class": EnhancedPaginator,
            "per_page": get_paginate_count(request),
        }
        RequestConfig(request, paginate).configure(notes_table)

        self.base_template = get_base_template(self.base_template, model)

        return render(
            request,
            "extras/object_notes.html",
            {
                "object": obj,
                "verbose_name": obj._meta.verbose_name,
                "verbose_name_plural": obj._meta.verbose_name_plural,
                "table": notes_table,
                "base_template": self.base_template,
                "active_tab": "notes",
                "form": notes_form,
            },
        )

nautobot.apps.views.ObjectNotesViewMixin

Bases: NautobotViewSetMixin

UI Mixin for an Object's Notes.

Source code in nautobot/core/views/mixins.py
class ObjectNotesViewMixin(NautobotViewSetMixin):
    """
    UI Mixin for an Object's Notes.
    """

    base_template = None

    @drf_action(detail=True)
    def notes(self, request, *args, **kwargs):
        data = {
            "base_template": self.base_template,
        }
        return Response(data)

nautobot.apps.views.ObjectPermissionRequiredMixin

Bases: AccessMixin

Similar to Django's built-in PermissionRequiredMixin, but extended to check for both model-level and object-level permission assignments. If the user has only object-level permissions assigned, the view's queryset is filtered to return only those objects on which the user is permitted to perform the specified action.

An optional iterable of statically declared permissions to evaluate in addition to those

derived from the object type

Source code in nautobot/core/views/mixins.py
class ObjectPermissionRequiredMixin(AccessMixin):
    """
    Similar to Django's built-in PermissionRequiredMixin, but extended to check for both model-level and object-level
    permission assignments. If the user has only object-level permissions assigned, the view's queryset is filtered
    to return only those objects on which the user is permitted to perform the specified action.

    additional_permissions: An optional iterable of statically declared permissions to evaluate in addition to those
                            derived from the object type
    """

    additional_permissions = []

    def get_required_permission(self):
        """
        Return the specific permission necessary to perform the requested action on an object.
        """
        raise NotImplementedError(f"{self.__class__.__name__} must implement get_required_permission()")

    def has_permission(self):
        user = self.request.user
        permission_required = self.get_required_permission()

        # Check that the user has been granted the required permission(s).
        if user.has_perms((permission_required, *self.additional_permissions)):
            # Update the view's QuerySet to filter only the permitted objects
            action = permissions.resolve_permission(permission_required)[1]
            self.queryset = self.queryset.restrict(user, action)

            return True

        return False

    def dispatch(self, request, *args, **kwargs):
        if not hasattr(self, "queryset"):
            raise ImproperlyConfigured(
                (
                    f"{self.__class__.__name__} has no queryset defined. "
                    "ObjectPermissionRequiredMixin may only be used on views which define a base queryset"
                )
            )

        if not self.has_permission():
            return self.handle_no_permission()

        return super().dispatch(request, *args, **kwargs)

get_required_permission()

Return the specific permission necessary to perform the requested action on an object.

Source code in nautobot/core/views/mixins.py
def get_required_permission(self):
    """
    Return the specific permission necessary to perform the requested action on an object.
    """
    raise NotImplementedError(f"{self.__class__.__name__} must implement get_required_permission()")

nautobot.apps.views.ObjectView

Bases: ObjectPermissionRequiredMixin, View

Retrieve a single object for display.

queryset: The base queryset for retrieving the object template_name: Name of the template to use

Source code in nautobot/core/views/generic.py
class ObjectView(ObjectPermissionRequiredMixin, View):
    """
    Retrieve a single object for display.

    queryset: The base queryset for retrieving the object
    template_name: Name of the template to use
    """

    queryset = None
    template_name = None

    def get_required_permission(self):
        return get_permission_for_model(self.queryset.model, "view")

    def get_template_name(self):
        """
        Return self.template_name if set. Otherwise, resolve the template path by model app_label and name.
        """
        if self.template_name is not None:
            return self.template_name
        model_opts = self.queryset.model._meta
        return f"{model_opts.app_label}/{model_opts.model_name}.html"

    def get_extra_context(self, request, instance):
        """
        Return any additional context data for the template.

        Args:
            request (Request): The current request
            instance (Model): The object being viewed

        Returns:
            (dict): Additional context data
        """
        return {
            "active_tab": request.GET.get("tab", "main"),
        }

    def get(self, request, *args, **kwargs):
        """
        Generic GET handler for accessing an object.
        """
        instance = get_object_or_404(self.queryset, **kwargs)
        # Get the ObjectChange records to populate the advanced tab information
        created_by, last_updated_by = get_created_and_last_updated_usernames_for_model(instance)

        # TODO: this feels inelegant - should the tabs lookup be a dedicated endpoint rather than piggybacking
        # on the object-retrieve endpoint?
        # TODO: similar functionality probably needed in NautobotUIViewSet as well, not currently present
        if request.GET.get("viewconfig", None) == "true":
            # TODO: we shouldn't be importing a private-named function from another module. Should it be renamed?
            from nautobot.extras.templatetags.plugins import _get_registered_content

            temp_fake_context = {
                "object": instance,
                "request": request,
                "settings": {},
                "csrf_token": "",
                "perms": {},
            }

            plugin_tabs = _get_registered_content(instance, "detail_tabs", temp_fake_context, return_html=False)
            resp = {"tabs": plugin_tabs}
            return JsonResponse(resp)
        else:
            return render(
                request,
                self.get_template_name(),
                {
                    "object": instance,
                    "verbose_name": self.queryset.model._meta.verbose_name,
                    "verbose_name_plural": self.queryset.model._meta.verbose_name_plural,
                    "created_by": created_by,
                    "last_updated_by": last_updated_by,
                    **self.get_extra_context(request, instance),
                },
            )

get(request, *args, **kwargs)

Generic GET handler for accessing an object.

Source code in nautobot/core/views/generic.py
def get(self, request, *args, **kwargs):
    """
    Generic GET handler for accessing an object.
    """
    instance = get_object_or_404(self.queryset, **kwargs)
    # Get the ObjectChange records to populate the advanced tab information
    created_by, last_updated_by = get_created_and_last_updated_usernames_for_model(instance)

    # TODO: this feels inelegant - should the tabs lookup be a dedicated endpoint rather than piggybacking
    # on the object-retrieve endpoint?
    # TODO: similar functionality probably needed in NautobotUIViewSet as well, not currently present
    if request.GET.get("viewconfig", None) == "true":
        # TODO: we shouldn't be importing a private-named function from another module. Should it be renamed?
        from nautobot.extras.templatetags.plugins import _get_registered_content

        temp_fake_context = {
            "object": instance,
            "request": request,
            "settings": {},
            "csrf_token": "",
            "perms": {},
        }

        plugin_tabs = _get_registered_content(instance, "detail_tabs", temp_fake_context, return_html=False)
        resp = {"tabs": plugin_tabs}
        return JsonResponse(resp)
    else:
        return render(
            request,
            self.get_template_name(),
            {
                "object": instance,
                "verbose_name": self.queryset.model._meta.verbose_name,
                "verbose_name_plural": self.queryset.model._meta.verbose_name_plural,
                "created_by": created_by,
                "last_updated_by": last_updated_by,
                **self.get_extra_context(request, instance),
            },
        )

get_extra_context(request, instance)

Return any additional context data for the template.

Parameters:

Name Type Description Default
request Request

The current request

required
instance Model

The object being viewed

required

Returns:

Type Description
dict

Additional context data

Source code in nautobot/core/views/generic.py
def get_extra_context(self, request, instance):
    """
    Return any additional context data for the template.

    Args:
        request (Request): The current request
        instance (Model): The object being viewed

    Returns:
        (dict): Additional context data
    """
    return {
        "active_tab": request.GET.get("tab", "main"),
    }

get_template_name()

Return self.template_name if set. Otherwise, resolve the template path by model app_label and name.

Source code in nautobot/core/views/generic.py
def get_template_name(self):
    """
    Return self.template_name if set. Otherwise, resolve the template path by model app_label and name.
    """
    if self.template_name is not None:
        return self.template_name
    model_opts = self.queryset.model._meta
    return f"{model_opts.app_label}/{model_opts.model_name}.html"

nautobot.apps.views.check_and_call_git_repository_function(request, pk, func)

Helper for checking Git permissions and worker availability, then calling provided function if all is well

Parameters:

Name Type Description Default
request HttpRequest

request object.

required
pk UUID

GitRepository pk value.

required
func function

Enqueue git repo function.

required

Returns:

Type Description
Union[HttpResponseForbidden, redirect]

HttpResponseForbidden if user does not have permission to run the job, otherwise redirect to the job result page.

Source code in nautobot/extras/views.py
def check_and_call_git_repository_function(request, pk, func):
    """Helper for checking Git permissions and worker availability, then calling provided function if all is well
    Args:
        request (HttpRequest): request object.
        pk (UUID): GitRepository pk value.
        func (function): Enqueue git repo function.
    Returns:
        (Union[HttpResponseForbidden,redirect]): HttpResponseForbidden if user does not have permission to run the job,
            otherwise redirect to the job result page.
    """
    if not request.user.has_perm("extras.change_gitrepository"):
        return HttpResponseForbidden()

    # Allow execution only if a worker process is running.
    if not get_worker_count():
        messages.error(request, "Unable to run job: Celery worker process not running.")
        return redirect(request.get_full_path(), permanent=False)
    else:
        repository = get_object_or_404(GitRepository, pk=pk)
        job_result = func(repository, request.user)

    return redirect(job_result.get_absolute_url())

nautobot.apps.views.check_filter_for_display(filters, field_name, values)

Return any additional context data for the template.

Parameters:

Name Type Description Default
filters OrderedDict

The output of .get_filters() of a desired FilterSet

required
field_name str

The name of the filter to get a label for and lookup values

required
values list[str]

List of strings that may be PKs to look up

required

Returns:

Type Description
dict

A dict containing: - name: (str) Field name - display: (str) Resolved field name, whether that's a field label or fallback to inputted field_name if label unavailable - values: (list) List of dictionaries with the same name and display keys

Source code in nautobot/core/views/utils.py
def check_filter_for_display(filters, field_name, values):
    """
    Return any additional context data for the template.

    Args:
        filters (OrderedDict): The output of `.get_filters()` of a desired FilterSet
        field_name (str): The name of the filter to get a label for and lookup values
        values (list[str]): List of strings that may be PKs to look up

    Returns:
        (dict): A dict containing:
            - name: (str) Field name
            - display: (str) Resolved field name, whether that's a field label or fallback to inputted `field_name` if label unavailable
            - values: (list) List of dictionaries with the same `name` and `display` keys
    """
    values = values if isinstance(values, (list, tuple)) else [values]

    resolved_filter = {
        "name": field_name,
        "display": field_name,
        "values": [{"name": value, "display": value} for value in values],
    }

    if field_name not in filters.keys():
        return resolved_filter

    filter_field = filters[field_name]

    resolved_filter["display"] = get_filter_field_label(filter_field)

    if len(values) == 0 or not hasattr(filter_field, "queryset") or not is_uuid(values[0]):
        return resolved_filter
    else:
        try:
            new_values = []
            for value in filter_field.queryset.filter(pk__in=values):
                new_values.append({"name": str(value.pk), "display": getattr(value, "display", str(value))})
            resolved_filter["values"] = new_values
        except (FieldError, AttributeError):
            pass

    return resolved_filter

nautobot.apps.views.csv_format(data)

Convert the given list of data to a CSV row string.

Encapsulate any data which contains a comma within double quotes.

Obsolete, as CSV rendering in Nautobot core is now handled by nautobot.core.api.renderers.NautobotCSVRenderer.

Source code in nautobot/core/views/utils.py
def csv_format(data):
    """
    Convert the given list of data to a CSV row string.

    Encapsulate any data which contains a comma within double quotes.

    Obsolete, as CSV rendering in Nautobot core is now handled by nautobot.core.api.renderers.NautobotCSVRenderer.
    """
    csv = []
    for value in data:
        # Represent None or False with empty string
        if value is None or value is False:
            csv.append("")
            continue

        # Convert dates to ISO format
        if isinstance(value, (datetime.date, datetime.datetime)):
            value = value.isoformat()

        # Force conversion to string first so we can check for any commas
        if not isinstance(value, str):
            value = f"{value}"

        # Double-quote the value if it contains a comma or line break
        if "," in value or "\n" in value:
            value = value.replace('"', '""')  # Escape double-quotes
            csv.append(f'"{value}"')
        else:
            csv.append(f"{value}")

    return ",".join(csv)

nautobot.apps.views.get_csv_form_fields_from_serializer_class(serializer_class)

From the given serializer class, build a list of field dicts suitable for rendering in the CSV import form.

Source code in nautobot/core/views/utils.py
def get_csv_form_fields_from_serializer_class(serializer_class):
    """From the given serializer class, build a list of field dicts suitable for rendering in the CSV import form."""
    serializer = serializer_class(context={"request": None, "depth": 0})
    fields = []
    # Note lots of "noqa: S308" in this function. That's `suspicious-mark-safe-usage`, but in all of the below cases
    # we control the input string and it's known to be safe, so mark_safe() is being used correctly here.
    for field_name, field in serializer.fields.items():
        if field.read_only:
            continue
        if field_name == "custom_fields":
            from nautobot.extras.choices import CustomFieldTypeChoices
            from nautobot.extras.models import CustomField

            cfs = CustomField.objects.get_for_model(serializer_class.Meta.model)
            for cf in cfs:
                cf_form_field = cf.to_form_field(set_initial=False)
                field_info = {
                    "name": cf.add_prefix_to_cf_key(),
                    "required": cf_form_field.required,
                    "label": cf_form_field.label,
                    "help_text": cf_form_field.help_text,
                }
                if cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
                    field_info["format"] = mark_safe("<code>true</code> or <code>false</code>")  # noqa: S308
                elif cf.type == CustomFieldTypeChoices.TYPE_DATE:
                    field_info["format"] = mark_safe("<code>YYYY-MM-DD</code>")  # noqa: S308
                elif cf.type == CustomFieldTypeChoices.TYPE_SELECT:
                    field_info["choices"] = {cfc.value: cfc.value for cfc in cf.custom_field_choices.all()}
                elif cf.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
                    field_info["format"] = mark_safe('<code>"value,value"</code>')  # noqa: S308
                    field_info["choices"] = {cfc.value: cfc.value for cfc in cf.custom_field_choices.all()}
                fields.append(field_info)
            continue

        field_info = {
            "name": field_name,
            "required": field.required,
            "label": field.label,
            "help_text": field.help_text,
        }
        if isinstance(field, serializers.BooleanField):
            field_info["format"] = mark_safe("<code>true</code> or <code>false</code>")  # noqa: S308
        elif isinstance(field, serializers.DateField):
            field_info["format"] = mark_safe("<code>YYYY-MM-DD</code>")  # noqa: S308
        elif isinstance(field, TimeZoneSerializerField):
            field_info["format"] = mark_safe(  # noqa: S308
                '<a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">available options</a>'
            )
        elif isinstance(field, serializers.ManyRelatedField):
            if field.field_name == "tags":
                field_info["format"] = mark_safe('<code>"name,name"</code> or <code>"UUID,UUID"</code>')  # noqa: S308
            elif isinstance(field.child_relation, ContentTypeField):
                field_info["format"] = mark_safe('<code>"app_label.model,app_label.model"</code>')  # noqa: S308
            else:
                field_info["format"] = mark_safe('<code>"UUID,UUID"</code>')  # noqa: S308
        elif isinstance(field, serializers.RelatedField):
            if isinstance(field, ContentTypeField):
                field_info["format"] = mark_safe("<code>app_label.model</code>")  # noqa: S308
            else:
                field_info["format"] = mark_safe("<code>UUID</code>")  # noqa: S308
        elif isinstance(field, (serializers.ListField, serializers.MultipleChoiceField)):
            field_info["format"] = mark_safe('<code>"value,value"</code>')  # noqa: S308
        elif isinstance(field, (serializers.DictField, serializers.JSONField)):
            pass  # Not trivial to specify a format as it could be a JSON dict or a comma-separated string

        if isinstance(field, ChoiceField):
            field_info["choices"] = field.choices

        fields.append(field_info)

    # Move all required fields to the start of the list
    # TODO this ordering should be defined by the serializer instead...
    fields = sorted(fields, key=lambda info: 1 if info["required"] else 2)
    return fields

nautobot.apps.views.get_paginate_count(request)

Determine the length of a page, using the following in order:

1. per_page URL query parameter
2. Saved user preference
3. PAGINATE_COUNT global setting.
Source code in nautobot/core/views/paginator.py
def get_paginate_count(request):
    """
    Determine the length of a page, using the following in order:

        1. per_page URL query parameter
        2. Saved user preference
        3. PAGINATE_COUNT global setting.
    """
    if "per_page" in request.GET:
        try:
            per_page = int(request.GET.get("per_page"))
            if request.user.is_authenticated:
                request.user.set_config("pagination.per_page", per_page, commit=True)
            return per_page
        except ValueError:
            pass

    if request.user.is_authenticated:
        return request.user.get_config("pagination.per_page", config.get_settings_or_config("PAGINATE_COUNT"))
    return config.get_settings_or_config("PAGINATE_COUNT")

nautobot.apps.views.handle_protectederror(obj_list, request, e)

Generate a user-friendly error message in response to a ProtectedError exception.

Source code in nautobot/core/views/utils.py
def handle_protectederror(obj_list, request, e):
    """
    Generate a user-friendly error message in response to a ProtectedError exception.
    """
    protected_objects = list(e.protected_objects)
    protected_count = len(protected_objects) if len(protected_objects) <= 50 else "More than 50"
    err_message = format_html(
        "Unable to delete <strong>{}</strong>. {} dependent objects were found: ",
        ", ".join(str(obj) for obj in obj_list),
        protected_count,
    )

    # Append dependent objects to error message
    err_message += format_html_join(
        ", ",
        '<a href="{}">{}</a>',
        ((dependent.get_absolute_url(), dependent) for dependent in protected_objects[:50]),
    )

    messages.error(request, err_message)

nautobot.apps.views.prepare_cloned_fields(instance)

Compile an object's clone_fields list into a string of URL query parameters. Tags are automatically cloned where applicable.

Source code in nautobot/core/views/utils.py
def prepare_cloned_fields(instance):
    """
    Compile an object's `clone_fields` list into a string of URL query parameters. Tags are automatically cloned where
    applicable.
    """
    form_class = get_form_for_model(instance)
    form = form_class() if form_class is not None else None
    params = []
    for field_name in getattr(instance, "clone_fields", []):
        field = instance._meta.get_field(field_name)
        field_value = field.value_from_object(instance)

        # For foreign-key fields, if the ModelForm's field has a defined `to_field_name`,
        # use that field from the related object instead of its PK.
        # Example: Location.parent, LocationForm().fields["parent"].to_field_name = "name", so use name rather than PK.
        if isinstance(field, ForeignKey):
            related_object = getattr(instance, field_name)
            if (
                related_object is not None
                and form is not None
                and field_name in form.fields
                and hasattr(form.fields[field_name], "to_field_name")
                and form.fields[field_name].to_field_name is not None
            ):
                field_value = getattr(related_object, form.fields[field_name].to_field_name)

        # Swap out False with URL-friendly value
        if field_value is False:
            field_value = ""

        # This is likely an m2m field
        if isinstance(field_value, list):
            for fv in field_value:
                item_value = getattr(fv, "pk", str(fv))  # pk or str()
                params.append((field_name, item_value))

        # Omit empty values
        elif field_value not in (None, ""):
            params.append((field_name, field_value))

    # Copy tags
    if is_taggable(instance):
        for tag in instance.tags.all():
            params.append(("tags", tag.pk))

    # Concatenate parameters into a URL query string
    param_string = "&".join([f"{k}={v}" for k, v in params])

    return param_string