Skip to content

# `nautobot.apps.views`

Utilities for apps to implement UI views.

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

     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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.ObjectBulkCreateViewMixin`

Bases: `NautobotViewSetMixin`

UI mixin to bulk create model instances.

Source code in `nautobot/core/views/mixins.py`

    766
    767
    768
    769
    770
    771
    772
    773
    774
    775
    776
    777
    778
    779
    780
    781
    782
    783
    784
    785
    786
    787
    788
    789
    790
    791
    792
    793
    794
    795
    796
    797
    798
    799
    800
    801
    802
    803
    804
    805
    806
    807
    808
    809
    810
    811
    812
    813
    814
    815
    816
    817
    818
    819
    820
    821
    822
    823
    824
    825
    826
    827
    class ObjectBulkCreateViewMixin(NautobotViewSetMixin):
        """
        UI mixin to bulk create model instances.
        """

        bulk_create_active_tab = "csv-data"
        bulk_create_form_class = None
        bulk_create_widget_attrs = {}

        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:
                    field_name = "csv_file"
                    # 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"
                else:
                    field_name = "csv_data"
                headers, records = form.cleaned_data[field_name]
                for row, data in enumerate(records, start=1):
                    obj_form = self.bulk_create_form_class(data, headers=headers)
                    restrict_form_fields(obj_form, request.user)

                    if obj_form.is_valid():
                        obj = self.form_save(obj_form)
                        new_objs.append(obj)
                    else:
                        for field, err in obj_form.errors.items():
                            form.add_error(field_name, f"Row {row} {field}: {err[0]}")
                        raise ValidationError("")

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

    694
    695
    696
    697
    698
    699
    700
    701
    702
    703
    704
    705
    706
    707
    708
    709
    710
    711
    712
    713
    714
    715
    716
    717
    718
    719
    720
    721
    722
    723
    724
    725
    726
    727
    728
    729
    730
    731
    732
    733
    734
    735
    736
    737
    738
    739
    740
    741
    742
    743
    744
    745
    746
    747
    748
    749
    750
    751
    752
    753
    754
    755
    756
    757
    758
    759
    760
    761
    762
    763
    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"):
                if self.filterset_class is not None:
                    self.pk_list = [obj.pk for obj in self.filterset_class(request.POST, model.objects.only("pk")).qs]
                else:
                    self.pk_list = model.objects.values_list("pk", flat=True)
            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`

    722
    723
    724
    725
    726
    727
    728
    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`

    730
    731
    732
    733
    734
    735
    736
    737
    738
    739
    740
    741
    742
    743
    744
    745
    746
    747
    748
    749
    750
    751
    752
    753
    754
    755
    756
    757
    758
    759
    760
    761
    762
    763
    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"):
            if self.filterset_class is not None:
                self.pk_list = [obj.pk for obj in self.filterset_class(request.POST, model.objects.only("pk")).qs]
            else:
                self.pk_list = model.objects.values_list("pk", flat=True)
        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`

    830
    831
    832
    833
    834
    835
    836
    837
    838
    839
    840
    841
    842
    843
    844
    845
    846
    847
    848
    849
    850
    851
    852
    853
    854
    855
    856
    857
    858
    859
    860
    861
    862
    863
    864
    865
    866
    867
    868
    869
    870
    871
    872
    873
    874
    875
    876
    877
    878
    879
    880
    881
    882
    883
    884
    885
    886
    887
    888
    889
    890
    891
    892
    893
    894
    895
    896
    897
    898
    899
    900
    901
    902
    903
    904
    905
    906
    907
    908
    909
    910
    911
    912
    913
    914
    915
    916
    917
    918
    919
    920
    921
    922
    923
    924
    925
    926
    927
    928
    929
    930
    931
    932
    933
    934
    935
    936
    937
    938
    939
    940
    941
    942
    943
    944
    945
    946
    947
    948
    949
    950
    951
    952
    953
    954
    955
    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")
            form_cf_to_key = {f"cf_{cf.slug}": cf.name for cf in CustomField.objects.get_for_model(model)}
            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[form_cf_to_key[field_name]] = None
                        elif form.cleaned_data.get(field_name) not in (None, "", []):
                            obj.cf[form_cf_to_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"):
                if self.filterset_class is not None:
                    self.pk_list = [obj.pk for obj in self.filterset_class(request.POST, model.objects.only("pk")).qs]
                else:
                    self.pk_list = model.objects.values_list("pk", flat=True)
            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`

    911
    912
    913
    914
    915
    916
    917
    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`

    920
    921
    922
    923
    924
    925
    926
    927
    928
    929
    930
    931
    932
    933
    934
    935
    936
    937
    938
    939
    940
    941
    942
    943
    944
    945
    946
    947
    948
    949
    950
    951
    952
    953
    954
    955
    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"):
            if self.filterset_class is not None:
                self.pk_list = [obj.pk for obj in self.filterset_class(request.POST, model.objects.only("pk")).qs]
            else:
                self.pk_list = model.objects.values_list("pk", flat=True)
        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`

    958
    959
    960
    961
    962
    963
    964
    965
    966
    967
    968
    969
    970
    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.ObjectDestroyViewMixin`

Bases: `NautobotViewSetMixin`, `mixins.DestroyModelMixin`

UI mixin to destroy a model instance.

Source code in `nautobot/core/views/mixins.py`

    555
    556
    557
    558
    559
    560
    561
    562
    563
    564
    565
    566
    567
    568
    569
    570
    571
    572
    573
    574
    575
    576
    577
    578
    579
    580
    581
    582
    583
    584
    585
    586
    587
    588
    589
    590
    591
    592
    593
    594
    595
    596
    597
    598
    599
    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`

    578
    579
    580
    581
    582
    583
    584
    585
    586
    587
    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`

    589
    590
    591
    592
    593
    594
    595
    596
    597
    598
    599
    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`

    432
    433
    434
    435
    class ObjectDetailViewMixin(NautobotViewSetMixin, mixins.RetrieveModelMixin):
        """
        UI mixin to retrieve a model instance.
        """

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

    602
    603
    604
    605
    606
    607
    608
    609
    610
    611
    612
    613
    614
    615
    616
    617
    618
    619
    620
    621
    622
    623
    624
    625
    626
    627
    628
    629
    630
    631
    632
    633
    634
    635
    636
    637
    638
    639
    640
    641
    642
    643
    644
    645
    646
    647
    648
    649
    650
    651
    652
    653
    654
    655
    656
    657
    658
    659
    660
    661
    662
    663
    664
    665
    666
    667
    668
    669
    670
    671
    672
    673
    674
    675
    676
    677
    678
    679
    680
    681
    682
    683
    684
    685
    686
    687
    688
    689
    690
    691
    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})")
                if hasattr(obj, "get_absolute_url"):
                    msg = format_html('{} <a href="{}">{}</a>', msg, obj.get_absolute_url(), obj)
                else:
                    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 return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
                        self.success_url = 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`

    643
    644
    645
    646
    647
    648
    649
    650
    651
    652
    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`

    655
    656
    657
    658
    659
    660
    661
    662
    663
    664
    665
    666
    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`

    680
    681
    682
    683
    684
    685
    686
    687
    688
    689
    690
    691
    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`

    668
    669
    670
    671
    672
    673
    674
    675
    676
    677
    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.ObjectListViewMixin`

Bases: `NautobotViewSetMixin`, `mixins.ListModelMixin`

UI mixin to list a model queryset

Source code in `nautobot/core/views/mixins.py`

    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    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
            "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

        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

            # Fall back to built-in CSV formatting if export requested but no template specified
            elif "export" in request.GET and hasattr(model, "to_csv"):
                response = HttpResponse(self.queryset_to_csv(), content_type="text/csv")
                filename = f"nautobot_{queryset.model._meta.verbose_name_plural}.csv"
                response["Content-Disposition"] = f'attachment; filename="{filename}"'
                return response

            return None

        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 queryset_to_csv(self):
            """
            Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method.
            """
            queryset = self.filter_queryset(self.get_queryset())
            csv_data = []
            custom_fields = []
            # Start with the column headers
            headers = queryset.model.csv_headers.copy()

            # Add custom field headers, if any
            if hasattr(queryset.model, "_custom_field_data"):
                for custom_field in CustomField.objects.get_for_model(queryset.model):
                    headers.append("cf_" + custom_field.slug)
                    custom_fields.append(custom_field.name)

            csv_data.append(",".join(headers))

            # Iterate through the queryset appending each object
            for obj in queryset:
                data = obj.to_csv()

                for custom_field in custom_fields:
                    data += (obj.cf.get(custom_field, ""),)

                csv_data.append(csv_format(data))

            return "\n".join(csv_data)

        def list(self, request, *args, **kwargs):
            """
            List the model instances.
            """
            context = {}
            if "export" in request.GET:
                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`

    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    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`

    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    def list(self, request, *args, **kwargs):
        """
        List the model instances.
        """
        context = {}
        if "export" in request.GET:
            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_csv()`

Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method.

Source code in `nautobot/core/views/mixins.py`

    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    536
    537
    538
    def queryset_to_csv(self):
        """
        Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method.
        """
        queryset = self.filter_queryset(self.get_queryset())
        csv_data = []
        custom_fields = []
        # Start with the column headers
        headers = queryset.model.csv_headers.copy()

        # Add custom field headers, if any
        if hasattr(queryset.model, "_custom_field_data"):
            for custom_field in CustomField.objects.get_for_model(queryset.model):
                headers.append("cf_" + custom_field.slug)
                custom_fields.append(custom_field.name)

        csv_data.append(",".join(headers))

        # Iterate through the queryset appending each object
        for obj in queryset:
            data = obj.to_csv()

            for custom_field in custom_fields:
                data += (obj.cf.get(custom_field, ""),)

            csv_data.append(csv_format(data))

        return "\n".join(csv_data)

### `queryset_to_yaml()`

Export the queryset of objects as concatenated YAML documents.

Source code in `nautobot/core/views/mixins.py`

    502
    503
    504
    505
    506
    507
    508
    509
    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.ObjectNotesViewMixin`

Bases: `NautobotViewSetMixin`

UI Mixin for an Object's Notes.

Source code in `nautobot/core/views/mixins.py`

    973
    974
    975
    976
    977
    978
    979
    980
    981
    982
    983
    984
    985
    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.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`

     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    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"),
            }

        # 2.0 TODO: Remove this method in 2.0. Can be retrieved from instance itself now
        # instance.get_changelog_url()
        # Only available on models that support changelogs
        def get_changelog_url(self, instance):
            """Return the changelog URL for a given instance."""
            meta = self.queryset.model._meta

            # Don't try to generate a changelog_url for an ObjectChange.
            if meta.model_name == "objectchange":
                return None

            route = get_route_for_model(instance, "changelog")

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

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

            # This object likely doesn't have a changelog route defined.
            return None

        def get(self, request, *args, **kwargs):
            """
            Generic GET handler for accessing an object by PK or slug
            """
            instance = get_object_or_404(self.queryset, **kwargs)

            changelog_url = None

            if isinstance(instance, ChangeLoggedModel):
                changelog_url = instance.get_changelog_url()

            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,
                    "changelog_url": changelog_url,  # 2.0 TODO: Remove in 2.0. This information can be retrieved from the object itself now.
                    **self.get_extra_context(request, instance),
                },
            )

### `get(request, *args, **kwargs)`

Generic GET handler for accessing an object by PK or slug

Source code in `nautobot/core/views/generic.py`

    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    def get(self, request, *args, **kwargs):
        """
        Generic GET handler for accessing an object by PK or slug
        """
        instance = get_object_or_404(self.queryset, **kwargs)

        changelog_url = None

        if isinstance(instance, ChangeLoggedModel):
            changelog_url = instance.get_changelog_url()

        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,
                "changelog_url": changelog_url,  # 2.0 TODO: Remove in 2.0. This information can be retrieved from the object itself now.
                **self.get_extra_context(request, instance),
            },
        )

### `get_changelog_url(instance)`

Return the changelog URL for a given instance.

Source code in `nautobot/core/views/generic.py`

     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    def get_changelog_url(self, instance):
        """Return the changelog URL for a given instance."""
        meta = self.queryset.model._meta

        # Don't try to generate a changelog_url for an ObjectChange.
        if meta.model_name == "objectchange":
            return None

        route = get_route_for_model(instance, "changelog")

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

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

        # This object likely doesn't have a changelog route defined.
        return None

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

    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    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`

    70
    71
    72
    73
    74
    75
    76
    77
    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"