nautobot.apps.views - Nautobot Documentation

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`

    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    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`

    1388
    1389
    1390
    1391
    1392
    1393
    1394
    1395
    1396
    1397
    1398
    1399
    1400
    1401
    1402
    1403
    1404
    1405
    1406
    1407
    1408
    1409
    1410
    1411
    1412
    1413
    1414
    1415
    1416
    1417
    1418
    1419
    1420
    1421
    1422
    1423
    1424
    1425
    1426
    1427
    1428
    1429
    1430
    1431
    1432
    1433
    1434
    1435
    1436
    1437
    1438
    1439
    1440
    1441
    1442
    1443
    1444
    1445
    1446
    1447
    1448
    1449
    1450
    1451
    1452
    1453
    1454
    1455
    1456
    1457
    1458
    1459
    1460
    1461
    1462
    1463
    1464
    1465
    1466
    1467
    1468
    1469
    1470
    1471
    1472
    1473
    1474
    1475
    1476
    1477
    1478
    1479
    1480
    1481
    1482
    1483
    1484
    1485
    1486
    1487
    1488
    1489
    1490
    1491
    1492
    1493
    1494
    1495
    1496
    1497
    1498
    1499
    1500
    1501
    1502
    1503
    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`

    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    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
    600
    601
    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
    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`

    1170
    1171
    1172
    1173
    1174
    1175
    1176
    1177
    1178
    1179
    1180
    1181
    1182
    1183
    1184
    1185
    1186
    1187
    1188
    1189
    1190
    1191
    1192
    1193
    1194
    1195
    1196
    1197
    1198
    1199
    1200
    1201
    1202
    1203
    1204
    1205
    1206
    1207
    1208
    1209
    1210
    1211
    1212
    1213
    1214
    1215
    1216
    1217
    1218
    1219
    1220
    1221
    1222
    1223
    1224
    1225
    1226
    1227
    1228
    1229
    1230
    1231
    1232
    1233
    1234
    1235
    1236
    1237
    1238
    1239
    1240
    1241
    1242
    1243
    1244
    1245
    1246
    1247
    1248
    1249
    1250
    1251
    1252
    1253
    1254
    1255
    1256
    1257
    1258
    1259
    1260
    1261
    1262
    1263
    1264
    1265
    1266
    1267
    1268
    1269
    1270
    1271
    1272
    1273
    1274
    1275
    1276
    1277
    1278
    1279
    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`

    1268
    1269
    1270
    1271
    1272
    1273
    1274
    1275
    1276
    1277
    1278
    1279
    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`

     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
     956
     957
     958
     959
     960
     961
     962
     963
     964
     965
     966
     967
     968
     969
     970
     971
     972
     973
     974
     975
     976
     977
     978
     979
     980
     981
     982
     983
     984
     985
     986
     987
     988
     989
     990
     991
     992
     993
     994
     995
     996
     997
     998
     999
    1000
    1001
    1002
    1003
    1004
    1005
    1006
    1007
    1008
    1009
    1010
    1011
    1012
    1013
    1014
    1015
    1016
    1017
    1018
    1019
    1020
    1021
    1022
    1023
    1024
    1025
    1026
    1027
    1028
    1029
    1030
    1031
    1032
    1033
    1034
    1035
    1036
    1037
    1038
    1039
    1040
    1041
    1042
    1043
    1044
    1045
    1046
    1047
    1048
    1049
    1050
    1051
    1052
    1053
    1054
    1055
    1056
    1057
    1058
    1059
    1060
    1061
    1062
    1063
    1064
    1065
    1066
    1067
    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`

    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
    828
    829
    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
    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`

    1070
    1071
    1072
    1073
    1074
    1075
    1076
    1077
    1078
    1079
    1080
    1081
    1082
    1083
    1084
    1085
    1086
    1087
    1088
    1089
    1090
    1091
    1092
    1093
    1094
    1095
    1096
    1097
    1098
    1099
    1100
    1101
    1102
    1103
    1104
    1105
    1106
    1107
    1108
    1109
    1110
    1111
    1112
    1113
    1114
    1115
    1116
    1117
    1118
    1119
    1120
    1121
    1122
    1123
    1124
    1125
    1126
    1127
    1128
    1129
    1130
    1131
    1132
    1133
    1134
    1135
    1136
    1137
    1138
    1139
    1140
    1141
    1142
    1143
    1144
    1145
    1146
    1147
    1148
    1149
    1150
    1151
    1152
    1153
    1154
    1155
    1156
    1157
    1158
    1159
    1160
    1161
    1162
    1163
    1164
    1165
    1166
    1167
    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`

    1154
    1155
    1156
    1157
    1158
    1159
    1160
    1161
    1162
    1163
    1164
    1165
    1166
    1167
    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`

    1288
    1289
    1290
    1291
    1292
    1293
    1294
    1295
    1296
    1297
    1298
    1299
    1300
    1301
    1302
    1303
    1304
    1305
    1306
    1307
    1308
    1309
    1310
    1311
    1312
    1313
    1314
    1315
    1316
    1317
    1318
    1319
    1320
    1321
    1322
    1323
    1324
    1325
    1326
    1327
    1328
    1329
    1330
    1331
    1332
    1333
    1334
    1335
    1336
    1337
    1338
    1339
    1340
    1341
    1342
    1343
    1344
    1345
    1346
    1347
    1348
    1349
    1350
    1351
    1352
    1353
    1354
    1355
    1356
    1357
    1358
    1359
    1360
    1361
    1362
    1363
    1364
    1365
    1366
    1367
    1368
    1369
    1370
    1371
    1372
    1373
    1374
    1375
    1376
    1377
    1378
    1379
    1380
    1381
    1382
    1383
    1384
    1385
    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`

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

    79
    80
    81
    82
    83
    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`

    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    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`

     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    class 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`

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

    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    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`

    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    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`

    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    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`

    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    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`

    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    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`

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

    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    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
    553
    554
    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
    @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`

    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    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`

    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    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`

    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    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`

    572
    573
    574
    575
    576
    577
    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`

    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    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`

    477
    478
    479
    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`

    481
    482
    483
    484
    485
    486
    487
    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`

    459
    460
    461
    462
    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`

    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    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`

    532
    533
    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    555
    556
    557
    558
    559
    560
    561
    562
    563
    564
    565
    566
    567
    568
    569
    570
    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`

    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    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`

    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    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`

    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    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`

    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    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`

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

    824
    825
    826
    827
    828
    829
    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
    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`

    852
    853
    854
    855
    856
    857
    858
    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`

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

     947
     948
     949
     950
     951
     952
     953
     954
     955
     956
     957
     958
     959
     960
     961
     962
     963
     964
     965
     966
     967
     968
     969
     970
     971
     972
     973
     974
     975
     976
     977
     978
     979
     980
     981
     982
     983
     984
     985
     986
     987
     988
     989
     990
     991
     992
     993
     994
     995
     996
     997
     998
     999
    1000
    1001
    1002
    1003
    1004
    1005
    1006
    1007
    1008
    1009
    1010
    1011
    1012
    1013
    1014
    1015
    1016
    1017
    1018
    1019
    1020
    1021
    1022
    1023
    1024
    1025
    1026
    1027
    1028
    1029
    1030
    1031
    1032
    1033
    1034
    1035
    1036
    1037
    1038
    1039
    1040
    1041
    1042
    1043
    1044
    1045
    1046
    1047
    1048
    1049
    1050
    1051
    1052
    1053
    1054
    1055
    1056
    1057
    1058
    1059
    1060
    1061
    1062
    1063
    1064
    1065
    1066
    1067
    1068
    1069
    1070
    1071
    1072
    1073
    1074
    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`

    1027
    1028
    1029
    1030
    1031
    1032
    1033
    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`

    1036
    1037
    1038
    1039
    1040
    1041
    1042
    1043
    1044
    1045
    1046
    1047
    1048
    1049
    1050
    1051
    1052
    1053
    1054
    1055
    1056
    1057
    1058
    1059
    1060
    1061
    1062
    1063
    1064
    1065
    1066
    1067
    1068
    1069
    1070
    1071
    1072
    1073
    1074
    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`

    1077
    1078
    1079
    1080
    1081
    1082
    1083
    1084
    1085
    1086
    1087
    1088
    1089
    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`

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

    480
    481
    482
    483
    484
    485
    486
    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`

    685
    686
    687
    688
    689
    690
    691
    692
    693
    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
    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`

    708
    709
    710
    711
    712
    713
    714
    715
    716
    717
    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`

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

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

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

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

    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    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
    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`

    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    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`

    345
    346
    347
    348
    349
    350
    351
    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`

    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    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`

    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
    764
    765
    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
    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`

    773
    774
    775
    776
    777
    778
    779
    780
    781
    782
    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`

    785
    786
    787
    788
    789
    790
    791
    792
    793
    794
    795
    796
    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`

    810
    811
    812
    813
    814
    815
    816
    817
    818
    819
    820
    821
    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`

    798
    799
    800
    801
    802
    803
    804
    805
    806
    807
    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`

    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
    692
    693
    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
    764
    765
    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
    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`

    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    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`

    165
    166
    167
    168
    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`

    174
    175
    176
    177
    178
    179
    180
    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`

    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    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`

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

    617
    618
    619
    620
    621
    622
    623
    624
    625
    626
    627
    628
    629
    630
    631
    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`

    670
    671
    672
    673
    674
    675
    676
    677
    678
    679
    680
    681
    682
    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`

    661
    662
    663
    664
    665
    666
    667
    668
    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`

    1733
    1734
    1735
    1736
    1737
    1738
    1739
    1740
    1741
    1742
    1743
    1744
    1745
    1746
    1747
    1748
    1749
    1750
    1751
    1752
    1753
    1754
    1755
    1756
    1757
    1758
    1759
    1760
    1761
    1762
    1763
    1764
    1765
    1766
    1767
    1768
    1769
    1770
    1771
    1772
    1773
    1774
    1775
    1776
    1777
    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`

    1092
    1093
    1094
    1095
    1096
    1097
    1098
    1099
    1100
    1101
    1102
    1103
    1104
    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`

    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    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`

    133
    134
    135
    136
    137
    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`

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

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

    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    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`

    74
    75
    76
    77
    78
    79
    80
    81
    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`

    897
    898
    899
    900
    901
    902
    903
    904
    905
    906
    907
    908
    909
    910
    911
    912
    913
    914
    915
    916
    917
    918
    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`

    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    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`

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

     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    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`

    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    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`

    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    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`

    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    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