Просмотр исходного кода

Relocate and group generic views

Jeremy Stretch 5 лет назад
Родитель
Сommit
b3c770216e

+ 23 - 25
netbox/circuits/views.py

@@ -4,11 +4,9 @@ from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django_tables2 import RequestConfig
 from django_tables2 import RequestConfig
 
 
+from netbox.views import generic
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 from utilities.paginator import EnhancedPaginator, get_paginate_count
-from utilities.views import (
-    BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
-)
 from . import filters, forms, tables
 from . import filters, forms, tables
 from .choices import CircuitTerminationSideChoices
 from .choices import CircuitTerminationSideChoices
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -18,14 +16,14 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
 # Providers
 # Providers
 #
 #
 
 
-class ProviderListView(ObjectListView):
+class ProviderListView(generic.ObjectListView):
     queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering)
     queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering)
     filterset = filters.ProviderFilterSet
     filterset = filters.ProviderFilterSet
     filterset_form = forms.ProviderFilterForm
     filterset_form = forms.ProviderFilterForm
     table = tables.ProviderTable
     table = tables.ProviderTable
 
 
 
 
-class ProviderView(ObjectView):
+class ProviderView(generic.ObjectView):
     queryset = Provider.objects.all()
     queryset = Provider.objects.all()
 
 
     def get(self, request, slug):
     def get(self, request, slug):
@@ -52,30 +50,30 @@ class ProviderView(ObjectView):
         })
         })
 
 
 
 
-class ProviderEditView(ObjectEditView):
+class ProviderEditView(generic.ObjectEditView):
     queryset = Provider.objects.all()
     queryset = Provider.objects.all()
     model_form = forms.ProviderForm
     model_form = forms.ProviderForm
     template_name = 'circuits/provider_edit.html'
     template_name = 'circuits/provider_edit.html'
 
 
 
 
-class ProviderDeleteView(ObjectDeleteView):
+class ProviderDeleteView(generic.ObjectDeleteView):
     queryset = Provider.objects.all()
     queryset = Provider.objects.all()
 
 
 
 
-class ProviderBulkImportView(BulkImportView):
+class ProviderBulkImportView(generic.BulkImportView):
     queryset = Provider.objects.all()
     queryset = Provider.objects.all()
     model_form = forms.ProviderCSVForm
     model_form = forms.ProviderCSVForm
     table = tables.ProviderTable
     table = tables.ProviderTable
 
 
 
 
-class ProviderBulkEditView(BulkEditView):
+class ProviderBulkEditView(generic.BulkEditView):
     queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering)
     queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering)
     filterset = filters.ProviderFilterSet
     filterset = filters.ProviderFilterSet
     table = tables.ProviderTable
     table = tables.ProviderTable
     form = forms.ProviderBulkEditForm
     form = forms.ProviderBulkEditForm
 
 
 
 
-class ProviderBulkDeleteView(BulkDeleteView):
+class ProviderBulkDeleteView(generic.BulkDeleteView):
     queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering)
     queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering)
     filterset = filters.ProviderFilterSet
     filterset = filters.ProviderFilterSet
     table = tables.ProviderTable
     table = tables.ProviderTable
@@ -85,27 +83,27 @@ class ProviderBulkDeleteView(BulkDeleteView):
 # Circuit Types
 # Circuit Types
 #
 #
 
 
-class CircuitTypeListView(ObjectListView):
+class CircuitTypeListView(generic.ObjectListView):
     queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')).order_by(*CircuitType._meta.ordering)
     queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')).order_by(*CircuitType._meta.ordering)
     table = tables.CircuitTypeTable
     table = tables.CircuitTypeTable
 
 
 
 
-class CircuitTypeEditView(ObjectEditView):
+class CircuitTypeEditView(generic.ObjectEditView):
     queryset = CircuitType.objects.all()
     queryset = CircuitType.objects.all()
     model_form = forms.CircuitTypeForm
     model_form = forms.CircuitTypeForm
 
 
 
 
-class CircuitTypeDeleteView(ObjectDeleteView):
+class CircuitTypeDeleteView(generic.ObjectDeleteView):
     queryset = CircuitType.objects.all()
     queryset = CircuitType.objects.all()
 
 
 
 
-class CircuitTypeBulkImportView(BulkImportView):
+class CircuitTypeBulkImportView(generic.BulkImportView):
     queryset = CircuitType.objects.all()
     queryset = CircuitType.objects.all()
     model_form = forms.CircuitTypeCSVForm
     model_form = forms.CircuitTypeCSVForm
     table = tables.CircuitTypeTable
     table = tables.CircuitTypeTable
 
 
 
 
-class CircuitTypeBulkDeleteView(BulkDeleteView):
+class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
     queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')).order_by(*CircuitType._meta.ordering)
     queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')).order_by(*CircuitType._meta.ordering)
     table = tables.CircuitTypeTable
     table = tables.CircuitTypeTable
 
 
@@ -114,7 +112,7 @@ class CircuitTypeBulkDeleteView(BulkDeleteView):
 # Circuits
 # Circuits
 #
 #
 
 
-class CircuitListView(ObjectListView):
+class CircuitListView(generic.ObjectListView):
     queryset = Circuit.objects.prefetch_related(
     queryset = Circuit.objects.prefetch_related(
         'provider', 'type', 'tenant', 'terminations'
         'provider', 'type', 'tenant', 'terminations'
     ).annotate_sites()
     ).annotate_sites()
@@ -123,7 +121,7 @@ class CircuitListView(ObjectListView):
     table = tables.CircuitTable
     table = tables.CircuitTable
 
 
 
 
-class CircuitView(ObjectView):
+class CircuitView(generic.ObjectView):
     queryset = Circuit.objects.all()
     queryset = Circuit.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -152,23 +150,23 @@ class CircuitView(ObjectView):
         })
         })
 
 
 
 
-class CircuitEditView(ObjectEditView):
+class CircuitEditView(generic.ObjectEditView):
     queryset = Circuit.objects.all()
     queryset = Circuit.objects.all()
     model_form = forms.CircuitForm
     model_form = forms.CircuitForm
     template_name = 'circuits/circuit_edit.html'
     template_name = 'circuits/circuit_edit.html'
 
 
 
 
-class CircuitDeleteView(ObjectDeleteView):
+class CircuitDeleteView(generic.ObjectDeleteView):
     queryset = Circuit.objects.all()
     queryset = Circuit.objects.all()
 
 
 
 
-class CircuitBulkImportView(BulkImportView):
+class CircuitBulkImportView(generic.BulkImportView):
     queryset = Circuit.objects.all()
     queryset = Circuit.objects.all()
     model_form = forms.CircuitCSVForm
     model_form = forms.CircuitCSVForm
     table = tables.CircuitTable
     table = tables.CircuitTable
 
 
 
 
-class CircuitBulkEditView(BulkEditView):
+class CircuitBulkEditView(generic.BulkEditView):
     queryset = Circuit.objects.prefetch_related(
     queryset = Circuit.objects.prefetch_related(
         'provider', 'type', 'tenant', 'terminations'
         'provider', 'type', 'tenant', 'terminations'
     )
     )
@@ -177,7 +175,7 @@ class CircuitBulkEditView(BulkEditView):
     form = forms.CircuitBulkEditForm
     form = forms.CircuitBulkEditForm
 
 
 
 
-class CircuitBulkDeleteView(BulkDeleteView):
+class CircuitBulkDeleteView(generic.BulkDeleteView):
     queryset = Circuit.objects.prefetch_related(
     queryset = Circuit.objects.prefetch_related(
         'provider', 'type', 'tenant', 'terminations'
         'provider', 'type', 'tenant', 'terminations'
     )
     )
@@ -185,7 +183,7 @@ class CircuitBulkDeleteView(BulkDeleteView):
     table = tables.CircuitTable
     table = tables.CircuitTable
 
 
 
 
-class CircuitSwapTerminations(ObjectEditView):
+class CircuitSwapTerminations(generic.ObjectEditView):
     """
     """
     Swap the A and Z terminations of a circuit.
     Swap the A and Z terminations of a circuit.
     """
     """
@@ -258,7 +256,7 @@ class CircuitSwapTerminations(ObjectEditView):
 # Circuit terminations
 # Circuit terminations
 #
 #
 
 
-class CircuitTerminationEditView(ObjectEditView):
+class CircuitTerminationEditView(generic.ObjectEditView):
     queryset = CircuitTermination.objects.all()
     queryset = CircuitTermination.objects.all()
     model_form = forms.CircuitTerminationForm
     model_form = forms.CircuitTerminationForm
     template_name = 'circuits/circuittermination_edit.html'
     template_name = 'circuits/circuittermination_edit.html'
@@ -272,5 +270,5 @@ class CircuitTerminationEditView(ObjectEditView):
         return obj.circuit.get_absolute_url()
         return obj.circuit.get_absolute_url()
 
 
 
 
-class CircuitTerminationDeleteView(ObjectDeleteView):
+class CircuitTerminationDeleteView(generic.ObjectDeleteView):
     queryset = CircuitTermination.objects.all()
     queryset = CircuitTermination.objects.all()

Разница между файлами не показана из-за своего большого размера
+ 137 - 141
netbox/dcim/views.py


+ 19 - 21
netbox/extras/views.py

@@ -10,14 +10,12 @@ from django_tables2 import RequestConfig
 from rq import Worker
 from rq import Worker
 
 
 from dcim.models import DeviceRole, Platform, Region, Site
 from dcim.models import DeviceRole, Platform, Region, Site
+from netbox.views import generic
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 from utilities.utils import copy_safe_request, shallow_compare_dict
 from utilities.utils import copy_safe_request, shallow_compare_dict
-from utilities.views import (
-    BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
-    ContentTypePermissionRequiredMixin,
-)
+from utilities.views import ContentTypePermissionRequiredMixin
 from virtualization.models import Cluster, ClusterGroup
 from virtualization.models import Cluster, ClusterGroup
 from . import filters, forms, tables
 from . import filters, forms, tables
 from .choices import JobResultStatusChoices
 from .choices import JobResultStatusChoices
@@ -30,7 +28,7 @@ from .scripts import get_scripts, run_script
 # Tags
 # Tags
 #
 #
 
 
-class TagListView(ObjectListView):
+class TagListView(generic.ObjectListView):
     queryset = Tag.objects.annotate(
     queryset = Tag.objects.annotate(
         items=Count('extras_taggeditem_items')
         items=Count('extras_taggeditem_items')
     ).order_by(*Tag._meta.ordering)
     ).order_by(*Tag._meta.ordering)
@@ -39,23 +37,23 @@ class TagListView(ObjectListView):
     table = tables.TagTable
     table = tables.TagTable
 
 
 
 
-class TagEditView(ObjectEditView):
+class TagEditView(generic.ObjectEditView):
     queryset = Tag.objects.all()
     queryset = Tag.objects.all()
     model_form = forms.TagForm
     model_form = forms.TagForm
     template_name = 'extras/tag_edit.html'
     template_name = 'extras/tag_edit.html'
 
 
 
 
-class TagDeleteView(ObjectDeleteView):
+class TagDeleteView(generic.ObjectDeleteView):
     queryset = Tag.objects.all()
     queryset = Tag.objects.all()
 
 
 
 
-class TagBulkImportView(BulkImportView):
+class TagBulkImportView(generic.BulkImportView):
     queryset = Tag.objects.all()
     queryset = Tag.objects.all()
     model_form = forms.TagCSVForm
     model_form = forms.TagCSVForm
     table = tables.TagTable
     table = tables.TagTable
 
 
 
 
-class TagBulkEditView(BulkEditView):
+class TagBulkEditView(generic.BulkEditView):
     queryset = Tag.objects.annotate(
     queryset = Tag.objects.annotate(
         items=Count('extras_taggeditem_items')
         items=Count('extras_taggeditem_items')
     ).order_by(*Tag._meta.ordering)
     ).order_by(*Tag._meta.ordering)
@@ -63,7 +61,7 @@ class TagBulkEditView(BulkEditView):
     form = forms.TagBulkEditForm
     form = forms.TagBulkEditForm
 
 
 
 
-class TagBulkDeleteView(BulkDeleteView):
+class TagBulkDeleteView(generic.BulkDeleteView):
     queryset = Tag.objects.annotate(
     queryset = Tag.objects.annotate(
         items=Count('extras_taggeditem_items')
         items=Count('extras_taggeditem_items')
     ).order_by(*Tag._meta.ordering)
     ).order_by(*Tag._meta.ordering)
@@ -74,7 +72,7 @@ class TagBulkDeleteView(BulkDeleteView):
 # Config contexts
 # Config contexts
 #
 #
 
 
-class ConfigContextListView(ObjectListView):
+class ConfigContextListView(generic.ObjectListView):
     queryset = ConfigContext.objects.all()
     queryset = ConfigContext.objects.all()
     filterset = filters.ConfigContextFilterSet
     filterset = filters.ConfigContextFilterSet
     filterset_form = forms.ConfigContextFilterForm
     filterset_form = forms.ConfigContextFilterForm
@@ -82,7 +80,7 @@ class ConfigContextListView(ObjectListView):
     action_buttons = ('add',)
     action_buttons = ('add',)
 
 
 
 
-class ConfigContextView(ObjectView):
+class ConfigContextView(generic.ObjectView):
     queryset = ConfigContext.objects.all()
     queryset = ConfigContext.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -116,29 +114,29 @@ class ConfigContextView(ObjectView):
         })
         })
 
 
 
 
-class ConfigContextEditView(ObjectEditView):
+class ConfigContextEditView(generic.ObjectEditView):
     queryset = ConfigContext.objects.all()
     queryset = ConfigContext.objects.all()
     model_form = forms.ConfigContextForm
     model_form = forms.ConfigContextForm
     template_name = 'extras/configcontext_edit.html'
     template_name = 'extras/configcontext_edit.html'
 
 
 
 
-class ConfigContextBulkEditView(BulkEditView):
+class ConfigContextBulkEditView(generic.BulkEditView):
     queryset = ConfigContext.objects.all()
     queryset = ConfigContext.objects.all()
     filterset = filters.ConfigContextFilterSet
     filterset = filters.ConfigContextFilterSet
     table = tables.ConfigContextTable
     table = tables.ConfigContextTable
     form = forms.ConfigContextBulkEditForm
     form = forms.ConfigContextBulkEditForm
 
 
 
 
-class ConfigContextDeleteView(ObjectDeleteView):
+class ConfigContextDeleteView(generic.ObjectDeleteView):
     queryset = ConfigContext.objects.all()
     queryset = ConfigContext.objects.all()
 
 
 
 
-class ConfigContextBulkDeleteView(BulkDeleteView):
+class ConfigContextBulkDeleteView(generic.BulkDeleteView):
     queryset = ConfigContext.objects.all()
     queryset = ConfigContext.objects.all()
     table = tables.ConfigContextTable
     table = tables.ConfigContextTable
 
 
 
 
-class ObjectConfigContextView(ObjectView):
+class ObjectConfigContextView(generic.ObjectView):
     base_template = None
     base_template = None
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -172,7 +170,7 @@ class ObjectConfigContextView(ObjectView):
 # Change logging
 # Change logging
 #
 #
 
 
-class ObjectChangeListView(ObjectListView):
+class ObjectChangeListView(generic.ObjectListView):
     queryset = ObjectChange.objects.all()
     queryset = ObjectChange.objects.all()
     filterset = filters.ObjectChangeFilterSet
     filterset = filters.ObjectChangeFilterSet
     filterset_form = forms.ObjectChangeFilterForm
     filterset_form = forms.ObjectChangeFilterForm
@@ -181,7 +179,7 @@ class ObjectChangeListView(ObjectListView):
     action_buttons = ('export',)
     action_buttons = ('export',)
 
 
 
 
-class ObjectChangeView(ObjectView):
+class ObjectChangeView(generic.ObjectView):
     queryset = ObjectChange.objects.all()
     queryset = ObjectChange.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -283,7 +281,7 @@ class ObjectChangeLogView(View):
 # Image attachments
 # Image attachments
 #
 #
 
 
-class ImageAttachmentEditView(ObjectEditView):
+class ImageAttachmentEditView(generic.ObjectEditView):
     queryset = ImageAttachment.objects.all()
     queryset = ImageAttachment.objects.all()
     model_form = forms.ImageAttachmentForm
     model_form = forms.ImageAttachmentForm
 
 
@@ -298,7 +296,7 @@ class ImageAttachmentEditView(ObjectEditView):
         return imageattachment.parent.get_absolute_url()
         return imageattachment.parent.get_absolute_url()
 
 
 
 
-class ImageAttachmentDeleteView(ObjectDeleteView):
+class ImageAttachmentDeleteView(generic.ObjectDeleteView):
     queryset = ImageAttachment.objects.all()
     queryset = ImageAttachment.objects.all()
 
 
     def get_return_url(self, request, imageattachment):
     def get_return_url(self, request, imageattachment):

+ 72 - 78
netbox/ipam/views.py

@@ -1,20 +1,14 @@
-import netaddr
-from django.conf import settings
 from django.db.models import Count, Prefetch
 from django.db.models import Count, Prefetch
 from django.db.models.expressions import RawSQL
 from django.db.models.expressions import RawSQL
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django_tables2 import RequestConfig
 from django_tables2 import RequestConfig
 
 
 from dcim.models import Device, Interface
 from dcim.models import Device, Interface
+from netbox.views import generic
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 from utilities.utils import get_subquery
 from utilities.utils import get_subquery
-from utilities.views import (
-    BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView,
-    ObjectListView,
-)
 from virtualization.models import VirtualMachine, VMInterface
 from virtualization.models import VirtualMachine, VMInterface
 from . import filters, forms, tables
 from . import filters, forms, tables
-from .choices import *
 from .constants import *
 from .constants import *
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
 from .utils import add_available_ipaddresses, add_available_prefixes, add_available_vlans
 from .utils import add_available_ipaddresses, add_available_prefixes, add_available_vlans
@@ -24,14 +18,14 @@ from .utils import add_available_ipaddresses, add_available_prefixes, add_availa
 # VRFs
 # VRFs
 #
 #
 
 
-class VRFListView(ObjectListView):
+class VRFListView(generic.ObjectListView):
     queryset = VRF.objects.all()
     queryset = VRF.objects.all()
     filterset = filters.VRFFilterSet
     filterset = filters.VRFFilterSet
     filterset_form = forms.VRFFilterForm
     filterset_form = forms.VRFFilterForm
     table = tables.VRFTable
     table = tables.VRFTable
 
 
 
 
-class VRFView(ObjectView):
+class VRFView(generic.ObjectView):
     queryset = VRF.objects.all()
     queryset = VRF.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -56,30 +50,30 @@ class VRFView(ObjectView):
         })
         })
 
 
 
 
-class VRFEditView(ObjectEditView):
+class VRFEditView(generic.ObjectEditView):
     queryset = VRF.objects.all()
     queryset = VRF.objects.all()
     model_form = forms.VRFForm
     model_form = forms.VRFForm
     template_name = 'ipam/vrf_edit.html'
     template_name = 'ipam/vrf_edit.html'
 
 
 
 
-class VRFDeleteView(ObjectDeleteView):
+class VRFDeleteView(generic.ObjectDeleteView):
     queryset = VRF.objects.all()
     queryset = VRF.objects.all()
 
 
 
 
-class VRFBulkImportView(BulkImportView):
+class VRFBulkImportView(generic.BulkImportView):
     queryset = VRF.objects.all()
     queryset = VRF.objects.all()
     model_form = forms.VRFCSVForm
     model_form = forms.VRFCSVForm
     table = tables.VRFTable
     table = tables.VRFTable
 
 
 
 
-class VRFBulkEditView(BulkEditView):
+class VRFBulkEditView(generic.BulkEditView):
     queryset = VRF.objects.prefetch_related('tenant')
     queryset = VRF.objects.prefetch_related('tenant')
     filterset = filters.VRFFilterSet
     filterset = filters.VRFFilterSet
     table = tables.VRFTable
     table = tables.VRFTable
     form = forms.VRFBulkEditForm
     form = forms.VRFBulkEditForm
 
 
 
 
-class VRFBulkDeleteView(BulkDeleteView):
+class VRFBulkDeleteView(generic.BulkDeleteView):
     queryset = VRF.objects.prefetch_related('tenant')
     queryset = VRF.objects.prefetch_related('tenant')
     filterset = filters.VRFFilterSet
     filterset = filters.VRFFilterSet
     table = tables.VRFTable
     table = tables.VRFTable
@@ -89,14 +83,14 @@ class VRFBulkDeleteView(BulkDeleteView):
 # Route targets
 # Route targets
 #
 #
 
 
-class RouteTargetListView(ObjectListView):
+class RouteTargetListView(generic.ObjectListView):
     queryset = RouteTarget.objects.all()
     queryset = RouteTarget.objects.all()
     filterset = filters.RouteTargetFilterSet
     filterset = filters.RouteTargetFilterSet
     filterset_form = forms.RouteTargetFilterForm
     filterset_form = forms.RouteTargetFilterForm
     table = tables.RouteTargetTable
     table = tables.RouteTargetTable
 
 
 
 
-class RouteTargetView(ObjectView):
+class RouteTargetView(generic.ObjectView):
     queryset = RouteTarget.objects.all()
     queryset = RouteTarget.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -118,29 +112,29 @@ class RouteTargetView(ObjectView):
         })
         })
 
 
 
 
-class RouteTargetEditView(ObjectEditView):
+class RouteTargetEditView(generic.ObjectEditView):
     queryset = RouteTarget.objects.all()
     queryset = RouteTarget.objects.all()
     model_form = forms.RouteTargetForm
     model_form = forms.RouteTargetForm
 
 
 
 
-class RouteTargetDeleteView(ObjectDeleteView):
+class RouteTargetDeleteView(generic.ObjectDeleteView):
     queryset = RouteTarget.objects.all()
     queryset = RouteTarget.objects.all()
 
 
 
 
-class RouteTargetBulkImportView(BulkImportView):
+class RouteTargetBulkImportView(generic.BulkImportView):
     queryset = RouteTarget.objects.all()
     queryset = RouteTarget.objects.all()
     model_form = forms.RouteTargetCSVForm
     model_form = forms.RouteTargetCSVForm
     table = tables.RouteTargetTable
     table = tables.RouteTargetTable
 
 
 
 
-class RouteTargetBulkEditView(BulkEditView):
+class RouteTargetBulkEditView(generic.BulkEditView):
     queryset = RouteTarget.objects.prefetch_related('tenant')
     queryset = RouteTarget.objects.prefetch_related('tenant')
     filterset = filters.RouteTargetFilterSet
     filterset = filters.RouteTargetFilterSet
     table = tables.RouteTargetTable
     table = tables.RouteTargetTable
     form = forms.RouteTargetBulkEditForm
     form = forms.RouteTargetBulkEditForm
 
 
 
 
-class RouteTargetBulkDeleteView(BulkDeleteView):
+class RouteTargetBulkDeleteView(generic.BulkDeleteView):
     queryset = RouteTarget.objects.prefetch_related('tenant')
     queryset = RouteTarget.objects.prefetch_related('tenant')
     filterset = filters.RouteTargetFilterSet
     filterset = filters.RouteTargetFilterSet
     table = tables.RouteTargetTable
     table = tables.RouteTargetTable
@@ -150,7 +144,7 @@ class RouteTargetBulkDeleteView(BulkDeleteView):
 # RIRs
 # RIRs
 #
 #
 
 
-class RIRListView(ObjectListView):
+class RIRListView(generic.ObjectListView):
     queryset = RIR.objects.annotate(aggregate_count=Count('aggregates')).order_by(*RIR._meta.ordering)
     queryset = RIR.objects.annotate(aggregate_count=Count('aggregates')).order_by(*RIR._meta.ordering)
     filterset = filters.RIRFilterSet
     filterset = filters.RIRFilterSet
     filterset_form = forms.RIRFilterForm
     filterset_form = forms.RIRFilterForm
@@ -158,22 +152,22 @@ class RIRListView(ObjectListView):
     template_name = 'ipam/rir_list.html'
     template_name = 'ipam/rir_list.html'
 
 
 
 
-class RIREditView(ObjectEditView):
+class RIREditView(generic.ObjectEditView):
     queryset = RIR.objects.all()
     queryset = RIR.objects.all()
     model_form = forms.RIRForm
     model_form = forms.RIRForm
 
 
 
 
-class RIRDeleteView(ObjectDeleteView):
+class RIRDeleteView(generic.ObjectDeleteView):
     queryset = RIR.objects.all()
     queryset = RIR.objects.all()
 
 
 
 
-class RIRBulkImportView(BulkImportView):
+class RIRBulkImportView(generic.BulkImportView):
     queryset = RIR.objects.all()
     queryset = RIR.objects.all()
     model_form = forms.RIRCSVForm
     model_form = forms.RIRCSVForm
     table = tables.RIRTable
     table = tables.RIRTable
 
 
 
 
-class RIRBulkDeleteView(BulkDeleteView):
+class RIRBulkDeleteView(generic.BulkDeleteView):
     queryset = RIR.objects.annotate(aggregate_count=Count('aggregates')).order_by(*RIR._meta.ordering)
     queryset = RIR.objects.annotate(aggregate_count=Count('aggregates')).order_by(*RIR._meta.ordering)
     filterset = filters.RIRFilterSet
     filterset = filters.RIRFilterSet
     table = tables.RIRTable
     table = tables.RIRTable
@@ -183,7 +177,7 @@ class RIRBulkDeleteView(BulkDeleteView):
 # Aggregates
 # Aggregates
 #
 #
 
 
-class AggregateListView(ObjectListView):
+class AggregateListView(generic.ObjectListView):
     queryset = Aggregate.objects.annotate(
     queryset = Aggregate.objects.annotate(
         child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
         child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
     ).order_by(*Aggregate._meta.ordering)
     ).order_by(*Aggregate._meta.ordering)
@@ -209,7 +203,7 @@ class AggregateListView(ObjectListView):
         }
         }
 
 
 
 
-class AggregateView(ObjectView):
+class AggregateView(generic.ObjectView):
     queryset = Aggregate.objects.all()
     queryset = Aggregate.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -254,30 +248,30 @@ class AggregateView(ObjectView):
         })
         })
 
 
 
 
-class AggregateEditView(ObjectEditView):
+class AggregateEditView(generic.ObjectEditView):
     queryset = Aggregate.objects.all()
     queryset = Aggregate.objects.all()
     model_form = forms.AggregateForm
     model_form = forms.AggregateForm
     template_name = 'ipam/aggregate_edit.html'
     template_name = 'ipam/aggregate_edit.html'
 
 
 
 
-class AggregateDeleteView(ObjectDeleteView):
+class AggregateDeleteView(generic.ObjectDeleteView):
     queryset = Aggregate.objects.all()
     queryset = Aggregate.objects.all()
 
 
 
 
-class AggregateBulkImportView(BulkImportView):
+class AggregateBulkImportView(generic.BulkImportView):
     queryset = Aggregate.objects.all()
     queryset = Aggregate.objects.all()
     model_form = forms.AggregateCSVForm
     model_form = forms.AggregateCSVForm
     table = tables.AggregateTable
     table = tables.AggregateTable
 
 
 
 
-class AggregateBulkEditView(BulkEditView):
+class AggregateBulkEditView(generic.BulkEditView):
     queryset = Aggregate.objects.prefetch_related('rir')
     queryset = Aggregate.objects.prefetch_related('rir')
     filterset = filters.AggregateFilterSet
     filterset = filters.AggregateFilterSet
     table = tables.AggregateTable
     table = tables.AggregateTable
     form = forms.AggregateBulkEditForm
     form = forms.AggregateBulkEditForm
 
 
 
 
-class AggregateBulkDeleteView(BulkDeleteView):
+class AggregateBulkDeleteView(generic.BulkDeleteView):
     queryset = Aggregate.objects.prefetch_related('rir')
     queryset = Aggregate.objects.prefetch_related('rir')
     filterset = filters.AggregateFilterSet
     filterset = filters.AggregateFilterSet
     table = tables.AggregateTable
     table = tables.AggregateTable
@@ -287,7 +281,7 @@ class AggregateBulkDeleteView(BulkDeleteView):
 # Prefix/VLAN roles
 # Prefix/VLAN roles
 #
 #
 
 
-class RoleListView(ObjectListView):
+class RoleListView(generic.ObjectListView):
     queryset = Role.objects.annotate(
     queryset = Role.objects.annotate(
         prefix_count=get_subquery(Prefix, 'role'),
         prefix_count=get_subquery(Prefix, 'role'),
         vlan_count=get_subquery(VLAN, 'role')
         vlan_count=get_subquery(VLAN, 'role')
@@ -295,22 +289,22 @@ class RoleListView(ObjectListView):
     table = tables.RoleTable
     table = tables.RoleTable
 
 
 
 
-class RoleEditView(ObjectEditView):
+class RoleEditView(generic.ObjectEditView):
     queryset = Role.objects.all()
     queryset = Role.objects.all()
     model_form = forms.RoleForm
     model_form = forms.RoleForm
 
 
 
 
-class RoleDeleteView(ObjectDeleteView):
+class RoleDeleteView(generic.ObjectDeleteView):
     queryset = Role.objects.all()
     queryset = Role.objects.all()
 
 
 
 
-class RoleBulkImportView(BulkImportView):
+class RoleBulkImportView(generic.BulkImportView):
     queryset = Role.objects.all()
     queryset = Role.objects.all()
     model_form = forms.RoleCSVForm
     model_form = forms.RoleCSVForm
     table = tables.RoleTable
     table = tables.RoleTable
 
 
 
 
-class RoleBulkDeleteView(BulkDeleteView):
+class RoleBulkDeleteView(generic.BulkDeleteView):
     queryset = Role.objects.all()
     queryset = Role.objects.all()
     table = tables.RoleTable
     table = tables.RoleTable
 
 
@@ -319,7 +313,7 @@ class RoleBulkDeleteView(BulkDeleteView):
 # Prefixes
 # Prefixes
 #
 #
 
 
-class PrefixListView(ObjectListView):
+class PrefixListView(generic.ObjectListView):
     queryset = Prefix.objects.annotate_tree()
     queryset = Prefix.objects.annotate_tree()
     filterset = filters.PrefixFilterSet
     filterset = filters.PrefixFilterSet
     filterset_form = forms.PrefixFilterForm
     filterset_form = forms.PrefixFilterForm
@@ -327,7 +321,7 @@ class PrefixListView(ObjectListView):
     template_name = 'ipam/prefix_list.html'
     template_name = 'ipam/prefix_list.html'
 
 
 
 
-class PrefixView(ObjectView):
+class PrefixView(generic.ObjectView):
     queryset = Prefix.objects.prefetch_related('vrf', 'site__region', 'tenant__group', 'vlan__group', 'role')
     queryset = Prefix.objects.prefetch_related('vrf', 'site__region', 'tenant__group', 'vlan__group', 'role')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -371,7 +365,7 @@ class PrefixView(ObjectView):
         })
         })
 
 
 
 
-class PrefixPrefixesView(ObjectView):
+class PrefixPrefixesView(generic.ObjectView):
     queryset = Prefix.objects.all()
     queryset = Prefix.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -415,7 +409,7 @@ class PrefixPrefixesView(ObjectView):
         })
         })
 
 
 
 
-class PrefixIPAddressesView(ObjectView):
+class PrefixIPAddressesView(generic.ObjectView):
     queryset = Prefix.objects.all()
     queryset = Prefix.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -459,31 +453,31 @@ class PrefixIPAddressesView(ObjectView):
         })
         })
 
 
 
 
-class PrefixEditView(ObjectEditView):
+class PrefixEditView(generic.ObjectEditView):
     queryset = Prefix.objects.all()
     queryset = Prefix.objects.all()
     model_form = forms.PrefixForm
     model_form = forms.PrefixForm
     template_name = 'ipam/prefix_edit.html'
     template_name = 'ipam/prefix_edit.html'
 
 
 
 
-class PrefixDeleteView(ObjectDeleteView):
+class PrefixDeleteView(generic.ObjectDeleteView):
     queryset = Prefix.objects.all()
     queryset = Prefix.objects.all()
     template_name = 'ipam/prefix_delete.html'
     template_name = 'ipam/prefix_delete.html'
 
 
 
 
-class PrefixBulkImportView(BulkImportView):
+class PrefixBulkImportView(generic.BulkImportView):
     queryset = Prefix.objects.all()
     queryset = Prefix.objects.all()
     model_form = forms.PrefixCSVForm
     model_form = forms.PrefixCSVForm
     table = tables.PrefixTable
     table = tables.PrefixTable
 
 
 
 
-class PrefixBulkEditView(BulkEditView):
+class PrefixBulkEditView(generic.BulkEditView):
     queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
     queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
     filterset = filters.PrefixFilterSet
     filterset = filters.PrefixFilterSet
     table = tables.PrefixTable
     table = tables.PrefixTable
     form = forms.PrefixBulkEditForm
     form = forms.PrefixBulkEditForm
 
 
 
 
-class PrefixBulkDeleteView(BulkDeleteView):
+class PrefixBulkDeleteView(generic.BulkDeleteView):
     queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
     queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
     filterset = filters.PrefixFilterSet
     filterset = filters.PrefixFilterSet
     table = tables.PrefixTable
     table = tables.PrefixTable
@@ -493,14 +487,14 @@ class PrefixBulkDeleteView(BulkDeleteView):
 # IP addresses
 # IP addresses
 #
 #
 
 
-class IPAddressListView(ObjectListView):
+class IPAddressListView(generic.ObjectListView):
     queryset = IPAddress.objects.all()
     queryset = IPAddress.objects.all()
     filterset = filters.IPAddressFilterSet
     filterset = filters.IPAddressFilterSet
     filterset_form = forms.IPAddressFilterForm
     filterset_form = forms.IPAddressFilterForm
     table = tables.IPAddressDetailTable
     table = tables.IPAddressDetailTable
 
 
 
 
-class IPAddressView(ObjectView):
+class IPAddressView(generic.ObjectView):
     queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
     queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -553,7 +547,7 @@ class IPAddressView(ObjectView):
         })
         })
 
 
 
 
-class IPAddressEditView(ObjectEditView):
+class IPAddressEditView(generic.ObjectEditView):
     queryset = IPAddress.objects.all()
     queryset = IPAddress.objects.all()
     model_form = forms.IPAddressForm
     model_form = forms.IPAddressForm
     template_name = 'ipam/ipaddress_edit.html'
     template_name = 'ipam/ipaddress_edit.html'
@@ -575,7 +569,7 @@ class IPAddressEditView(ObjectEditView):
         return obj
         return obj
 
 
 
 
-class IPAddressAssignView(ObjectView):
+class IPAddressAssignView(generic.ObjectView):
     """
     """
     Search for IPAddresses to be assigned to an Interface.
     Search for IPAddresses to be assigned to an Interface.
     """
     """
@@ -615,11 +609,11 @@ class IPAddressAssignView(ObjectView):
         })
         })
 
 
 
 
-class IPAddressDeleteView(ObjectDeleteView):
+class IPAddressDeleteView(generic.ObjectDeleteView):
     queryset = IPAddress.objects.all()
     queryset = IPAddress.objects.all()
 
 
 
 
-class IPAddressBulkCreateView(BulkCreateView):
+class IPAddressBulkCreateView(generic.BulkCreateView):
     queryset = IPAddress.objects.all()
     queryset = IPAddress.objects.all()
     form = forms.IPAddressBulkCreateForm
     form = forms.IPAddressBulkCreateForm
     model_form = forms.IPAddressBulkAddForm
     model_form = forms.IPAddressBulkAddForm
@@ -627,20 +621,20 @@ class IPAddressBulkCreateView(BulkCreateView):
     template_name = 'ipam/ipaddress_bulk_add.html'
     template_name = 'ipam/ipaddress_bulk_add.html'
 
 
 
 
-class IPAddressBulkImportView(BulkImportView):
+class IPAddressBulkImportView(generic.BulkImportView):
     queryset = IPAddress.objects.all()
     queryset = IPAddress.objects.all()
     model_form = forms.IPAddressCSVForm
     model_form = forms.IPAddressCSVForm
     table = tables.IPAddressTable
     table = tables.IPAddressTable
 
 
 
 
-class IPAddressBulkEditView(BulkEditView):
+class IPAddressBulkEditView(generic.BulkEditView):
     queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
     queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
     filterset = filters.IPAddressFilterSet
     filterset = filters.IPAddressFilterSet
     table = tables.IPAddressTable
     table = tables.IPAddressTable
     form = forms.IPAddressBulkEditForm
     form = forms.IPAddressBulkEditForm
 
 
 
 
-class IPAddressBulkDeleteView(BulkDeleteView):
+class IPAddressBulkDeleteView(generic.BulkDeleteView):
     queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
     queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
     filterset = filters.IPAddressFilterSet
     filterset = filters.IPAddressFilterSet
     table = tables.IPAddressTable
     table = tables.IPAddressTable
@@ -650,7 +644,7 @@ class IPAddressBulkDeleteView(BulkDeleteView):
 # VLAN groups
 # VLAN groups
 #
 #
 
 
-class VLANGroupListView(ObjectListView):
+class VLANGroupListView(generic.ObjectListView):
     queryset = VLANGroup.objects.annotate(
     queryset = VLANGroup.objects.annotate(
         vlan_count=Count('vlans')
         vlan_count=Count('vlans')
     ).order_by(*VLANGroup._meta.ordering)
     ).order_by(*VLANGroup._meta.ordering)
@@ -659,22 +653,22 @@ class VLANGroupListView(ObjectListView):
     table = tables.VLANGroupTable
     table = tables.VLANGroupTable
 
 
 
 
-class VLANGroupEditView(ObjectEditView):
+class VLANGroupEditView(generic.ObjectEditView):
     queryset = VLANGroup.objects.all()
     queryset = VLANGroup.objects.all()
     model_form = forms.VLANGroupForm
     model_form = forms.VLANGroupForm
 
 
 
 
-class VLANGroupDeleteView(ObjectDeleteView):
+class VLANGroupDeleteView(generic.ObjectDeleteView):
     queryset = VLANGroup.objects.all()
     queryset = VLANGroup.objects.all()
 
 
 
 
-class VLANGroupBulkImportView(BulkImportView):
+class VLANGroupBulkImportView(generic.BulkImportView):
     queryset = VLANGroup.objects.all()
     queryset = VLANGroup.objects.all()
     model_form = forms.VLANGroupCSVForm
     model_form = forms.VLANGroupCSVForm
     table = tables.VLANGroupTable
     table = tables.VLANGroupTable
 
 
 
 
-class VLANGroupBulkDeleteView(BulkDeleteView):
+class VLANGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = VLANGroup.objects.prefetch_related('site').annotate(
     queryset = VLANGroup.objects.prefetch_related('site').annotate(
         vlan_count=Count('vlans')
         vlan_count=Count('vlans')
     ).order_by(*VLANGroup._meta.ordering)
     ).order_by(*VLANGroup._meta.ordering)
@@ -682,7 +676,7 @@ class VLANGroupBulkDeleteView(BulkDeleteView):
     table = tables.VLANGroupTable
     table = tables.VLANGroupTable
 
 
 
 
-class VLANGroupVLANsView(ObjectView):
+class VLANGroupVLANsView(generic.ObjectView):
     queryset = VLANGroup.objects.all()
     queryset = VLANGroup.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -725,14 +719,14 @@ class VLANGroupVLANsView(ObjectView):
 # VLANs
 # VLANs
 #
 #
 
 
-class VLANListView(ObjectListView):
+class VLANListView(generic.ObjectListView):
     queryset = VLAN.objects.all()
     queryset = VLAN.objects.all()
     filterset = filters.VLANFilterSet
     filterset = filters.VLANFilterSet
     filterset_form = forms.VLANFilterForm
     filterset_form = forms.VLANFilterForm
     table = tables.VLANDetailTable
     table = tables.VLANDetailTable
 
 
 
 
-class VLANView(ObjectView):
+class VLANView(generic.ObjectView):
     queryset = VLAN.objects.prefetch_related('site__region', 'tenant__group', 'role')
     queryset = VLAN.objects.prefetch_related('site__region', 'tenant__group', 'role')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -750,7 +744,7 @@ class VLANView(ObjectView):
         })
         })
 
 
 
 
-class VLANInterfacesView(ObjectView):
+class VLANInterfacesView(generic.ObjectView):
     queryset = VLAN.objects.all()
     queryset = VLAN.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -771,7 +765,7 @@ class VLANInterfacesView(ObjectView):
         })
         })
 
 
 
 
-class VLANVMInterfacesView(ObjectView):
+class VLANVMInterfacesView(generic.ObjectView):
     queryset = VLAN.objects.all()
     queryset = VLAN.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -792,30 +786,30 @@ class VLANVMInterfacesView(ObjectView):
         })
         })
 
 
 
 
-class VLANEditView(ObjectEditView):
+class VLANEditView(generic.ObjectEditView):
     queryset = VLAN.objects.all()
     queryset = VLAN.objects.all()
     model_form = forms.VLANForm
     model_form = forms.VLANForm
     template_name = 'ipam/vlan_edit.html'
     template_name = 'ipam/vlan_edit.html'
 
 
 
 
-class VLANDeleteView(ObjectDeleteView):
+class VLANDeleteView(generic.ObjectDeleteView):
     queryset = VLAN.objects.all()
     queryset = VLAN.objects.all()
 
 
 
 
-class VLANBulkImportView(BulkImportView):
+class VLANBulkImportView(generic.BulkImportView):
     queryset = VLAN.objects.all()
     queryset = VLAN.objects.all()
     model_form = forms.VLANCSVForm
     model_form = forms.VLANCSVForm
     table = tables.VLANTable
     table = tables.VLANTable
 
 
 
 
-class VLANBulkEditView(BulkEditView):
+class VLANBulkEditView(generic.BulkEditView):
     queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
     queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
     filterset = filters.VLANFilterSet
     filterset = filters.VLANFilterSet
     table = tables.VLANTable
     table = tables.VLANTable
     form = forms.VLANBulkEditForm
     form = forms.VLANBulkEditForm
 
 
 
 
-class VLANBulkDeleteView(BulkDeleteView):
+class VLANBulkDeleteView(generic.BulkDeleteView):
     queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
     queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
     filterset = filters.VLANFilterSet
     filterset = filters.VLANFilterSet
     table = tables.VLANTable
     table = tables.VLANTable
@@ -825,7 +819,7 @@ class VLANBulkDeleteView(BulkDeleteView):
 # Services
 # Services
 #
 #
 
 
-class ServiceListView(ObjectListView):
+class ServiceListView(generic.ObjectListView):
     queryset = Service.objects.all()
     queryset = Service.objects.all()
     filterset = filters.ServiceFilterSet
     filterset = filters.ServiceFilterSet
     filterset_form = forms.ServiceFilterForm
     filterset_form = forms.ServiceFilterForm
@@ -833,7 +827,7 @@ class ServiceListView(ObjectListView):
     action_buttons = ('export',)
     action_buttons = ('export',)
 
 
 
 
-class ServiceView(ObjectView):
+class ServiceView(generic.ObjectView):
     queryset = Service.objects.prefetch_related('ipaddresses')
     queryset = Service.objects.prefetch_related('ipaddresses')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -845,7 +839,7 @@ class ServiceView(ObjectView):
         })
         })
 
 
 
 
-class ServiceEditView(ObjectEditView):
+class ServiceEditView(generic.ObjectEditView):
     queryset = Service.objects.prefetch_related('ipaddresses')
     queryset = Service.objects.prefetch_related('ipaddresses')
     model_form = forms.ServiceForm
     model_form = forms.ServiceForm
     template_name = 'ipam/service_edit.html'
     template_name = 'ipam/service_edit.html'
@@ -864,24 +858,24 @@ class ServiceEditView(ObjectEditView):
         return obj
         return obj
 
 
 
 
-class ServiceBulkImportView(BulkImportView):
+class ServiceBulkImportView(generic.BulkImportView):
     queryset = Service.objects.all()
     queryset = Service.objects.all()
     model_form = forms.ServiceCSVForm
     model_form = forms.ServiceCSVForm
     table = tables.ServiceTable
     table = tables.ServiceTable
 
 
 
 
-class ServiceDeleteView(ObjectDeleteView):
+class ServiceDeleteView(generic.ObjectDeleteView):
     queryset = Service.objects.all()
     queryset = Service.objects.all()
 
 
 
 
-class ServiceBulkEditView(BulkEditView):
+class ServiceBulkEditView(generic.BulkEditView):
     queryset = Service.objects.prefetch_related('device', 'virtual_machine')
     queryset = Service.objects.prefetch_related('device', 'virtual_machine')
     filterset = filters.ServiceFilterSet
     filterset = filters.ServiceFilterSet
     table = tables.ServiceTable
     table = tables.ServiceTable
     form = forms.ServiceBulkEditForm
     form = forms.ServiceBulkEditForm
 
 
 
 
-class ServiceBulkDeleteView(BulkDeleteView):
+class ServiceBulkDeleteView(generic.BulkDeleteView):
     queryset = Service.objects.prefetch_related('device', 'virtual_machine')
     queryset = Service.objects.prefetch_related('device', 'virtual_machine')
     filterset = filters.ServiceFilterSet
     filterset = filters.ServiceFilterSet
     table = tables.ServiceTable
     table = tables.ServiceTable

+ 1 - 1
netbox/netbox/middleware.py

@@ -8,8 +8,8 @@ from django.http import Http404, HttpResponseRedirect
 from django.urls import reverse
 from django.urls import reverse
 
 
 from extras.context_managers import change_logging
 from extras.context_managers import change_logging
+from netbox.views import server_error
 from utilities.api import is_api_request, rest_api_server_error
 from utilities.api import is_api_request, rest_api_server_error
-from utilities.views import server_error
 
 
 
 
 class LoginRequiredMiddleware(object):
 class LoginRequiredMiddleware(object):

+ 1 - 1
netbox/netbox/urls.py

@@ -94,4 +94,4 @@ urlpatterns = [
     path('{}'.format(settings.BASE_PATH), include(_patterns))
     path('{}'.format(settings.BASE_PATH), include(_patterns))
 ]
 ]
 
 
-handler500 = 'utilities.views.server_error'
+handler500 = 'netbox.views.server_error'

+ 32 - 3
netbox/netbox/views.py → netbox/netbox/views/__init__.py

@@ -1,22 +1,32 @@
+import platform
+import sys
+
 from django.conf import settings
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import F
 from django.db.models import F
+from django.http import HttpResponseServerError
 from django.shortcuts import render
 from django.shortcuts import render
+from django.template import loader
+from django.template.exceptions import TemplateDoesNotExist
 from django.urls import reverse
 from django.urls import reverse
+from django.views.decorators.csrf import requires_csrf_token
+from django.views.defaults import ERROR_500_TEMPLATE_NAME
 from django.views.generic import View
 from django.views.generic import View
 from packaging import version
 from packaging import version
 
 
 from circuits.models import Circuit, Provider
 from circuits.models import Circuit, Provider
-from dcim.models import Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, Site
+from dcim.models import (
+    Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, Site,
+)
 from extras.choices import JobResultStatusChoices
 from extras.choices import JobResultStatusChoices
 from extras.models import ObjectChange, JobResult
 from extras.models import ObjectChange, JobResult
 from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
 from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
+from netbox.constants import SEARCH_MAX_RESULTS, SEARCH_TYPES
+from netbox.forms import SearchForm
 from netbox.releases import get_latest_release
 from netbox.releases import get_latest_release
 from secrets.models import Secret
 from secrets.models import Secret
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from virtualization.models import Cluster, VirtualMachine
 from virtualization.models import Cluster, VirtualMachine
-from .constants import SEARCH_MAX_RESULTS, SEARCH_TYPES
-from .forms import SearchForm
 
 
 
 
 class HomeView(View):
 class HomeView(View):
@@ -157,3 +167,22 @@ class StaticMediaFailureView(View):
         return render(request, 'media_failure.html', {
         return render(request, 'media_failure.html', {
             'filename': request.GET.get('filename')
             'filename': request.GET.get('filename')
         })
         })
+
+
+@requires_csrf_token
+def server_error(request, template_name=ERROR_500_TEMPLATE_NAME):
+    """
+    Custom 500 handler to provide additional context when rendering 500.html.
+    """
+    try:
+        template = loader.get_template(template_name)
+    except TemplateDoesNotExist:
+        return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
+    type_, error, traceback = sys.exc_info()
+
+    return HttpResponseServerError(template.render({
+        'error': error,
+        'exception': str(type_),
+        'netbox_version': settings.VERSION,
+        'python_version': platform.python_version(),
+    }))

+ 1223 - 0
netbox/netbox/views/generic.py

@@ -0,0 +1,1223 @@
+import logging
+import re
+from copy import deepcopy
+
+from django.contrib import messages
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError
+from django.db import transaction, IntegrityError
+from django.db.models import ManyToManyField, ProtectedError
+from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
+from django.http import HttpResponse
+from django.shortcuts import get_object_or_404, redirect, render
+from django.utils.html import escape
+from django.utils.http import is_safe_url
+from django.utils.safestring import mark_safe
+from django.views.generic import View
+from django_tables2 import RequestConfig
+
+from extras.models import CustomField, ExportTemplate
+from utilities.error_handlers import handle_protectederror
+from utilities.exceptions import AbortTransaction
+from utilities.forms import (
+    BootstrapMixin, BulkRenameForm, ConfirmationForm, CSVDataField, ImportForm, TableConfigForm, restrict_form_fields,
+)
+from utilities.paginator import EnhancedPaginator, get_paginate_count
+from utilities.permissions import get_permission_for_model
+from utilities.utils import csv_format, normalize_querydict, prepare_cloned_fields
+from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
+
+
+class ObjectView(ObjectPermissionRequiredMixin, View):
+    """
+    Retrieve a single object for display.
+
+    queryset: The base queryset for retrieving the object.
+    """
+    queryset = 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 hasattr(self, 'template_name'):
+            return self.template_name
+        model_opts = self.queryset.model._meta
+        return f'{model_opts.app_label}/{model_opts.model_name}.html'
+
+    def get(self, request, pk):
+        """
+        Generic GET handler for accessing an object by PK
+        """
+        instance = get_object_or_404(self.queryset, pk=pk)
+
+        return render(request, self.get_template_name(), {
+            'instance': instance,
+        })
+
+
+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
+    """
+    queryset = None
+    filterset = None
+    filterset_form = None
+    table = None
+    template_name = 'utilities/obj_list.html'
+    action_buttons = ('add', 'import', 'export')
+
+    def get_required_permission(self):
+        return get_permission_for_model(self.queryset.model, 'view')
+
+    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 queryset_to_csv(self):
+        """
+        Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method.
+        """
+        csv_data = []
+        custom_fields = []
+
+        # Start with the column headers
+        headers = self.queryset.model.csv_headers.copy()
+
+        # Add custom field headers, if any
+        if hasattr(self.queryset.model, 'custom_field_data'):
+            for custom_field in CustomField.objects.get_for_model(self.queryset.model):
+                headers.append(custom_field.name)
+                custom_fields.append(custom_field.name)
+
+        csv_data.append(','.join(headers))
+
+        # Iterate through the queryset appending each object
+        for obj in self.queryset:
+            data = obj.to_csv()
+
+            for custom_field in custom_fields:
+                data += (obj.cf.get(custom_field, ''),)
+
+            csv_data.append(csv_format(data))
+
+        return '\n'.join(csv_data)
+
+    def get(self, request):
+
+        model = self.queryset.model
+        content_type = ContentType.objects.get_for_model(model)
+
+        if self.filterset:
+            self.queryset = self.filterset(request.GET, self.queryset).qs
+
+        # Check for export template rendering
+        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(self.queryset)
+            except Exception as e:
+                messages.error(
+                    request,
+                    "There was an error rendering the selected export template ({}): {}".format(
+                        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 = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural)
+            response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
+            return response
+
+        # Fall back to built-in CSV formatting if export requested but no template specified
+        elif 'export' in request.GET and hasattr(model, 'to_csv'):
+            response = HttpResponse(self.queryset_to_csv(), content_type='text/csv')
+            filename = 'netbox_{}.csv'.format(self.queryset.model._meta.verbose_name_plural)
+            response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
+            return response
+
+        # 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)
+
+        # Construct the objects table
+        table = self.table(self.queryset, user=request.user)
+        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)
+
+        context = {
+            'content_type': content_type,
+            'table': table,
+            'permissions': permissions,
+            'action_buttons': self.action_buttons,
+            'table_config_form': TableConfigForm(table=table),
+            'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
+        }
+        context.update(self.extra_context())
+
+        return render(request, self.template_name, context)
+
+    def extra_context(self):
+        return {}
+
+
+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 = 'utilities/obj_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):
+        # Look up an existing object by slug or PK, if provided.
+        if 'slug' in kwargs:
+            return get_object_or_404(self.queryset, slug=kwargs['slug'])
+        elif 'pk' in kwargs:
+            return get_object_or_404(self.queryset, pk=kwargs['pk'])
+        # Otherwise, return a new instance.
+        return self.queryset.model()
+
+    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 = 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),
+        })
+
+    def post(self, request, *args, **kwargs):
+        logger = logging.getLogger('netbox.views.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 = form.instance.pk is None
+                    obj = form.save()
+
+                    # Check that the new object conforms with any assigned object-level permissions
+                    self.queryset.get(pk=obj.pk)
+
+                msg = '{} {}'.format(
+                    'Created' if object_created else 'Modified',
+                    self.queryset.model._meta.verbose_name
+                )
+                logger.info(f"{msg} {obj} (PK: {obj.pk})")
+                if hasattr(obj, 'get_absolute_url'):
+                    msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
+                else:
+                    msg = '{} {}'.format(msg, escape(obj))
+                messages.success(request, mark_safe(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 = '{}?{}'.format(request.path, prepare_cloned_fields(obj))
+                        return redirect(url)
+
+                    return redirect(request.get_full_path())
+
+                return_url = form.cleaned_data.get('return_url')
+                if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
+                    return redirect(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),
+        })
+
+
+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 = 'utilities/obj_delete.html'
+
+    def get_required_permission(self):
+        return get_permission_for_model(self.queryset.model, 'delete')
+
+    def get_object(self, kwargs):
+        # Look up object by slug if one has been provided. Otherwise, use PK.
+        if 'slug' in kwargs:
+            return get_object_or_404(self.queryset, slug=kwargs['slug'])
+        else:
+            return get_object_or_404(self.queryset, pk=kwargs['pk'])
+
+    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('netbox.views.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 = 'Deleted {} {}'.format(self.queryset.model._meta.verbose_name, obj)
+            logger.info(msg)
+            messages.success(request, msg)
+
+            return_url = form.cleaned_data.get('return_url')
+            if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
+                return redirect(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),
+        })
+
+
+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('netbox.views.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 = "Added {} {}".format(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),
+        })
+
+
+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 = dict()
+    template_name = 'utilities/obj_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('netbox.views.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, list())):
+
+                                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 = "{}[{}] {}: {}".format(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, mark_safe('Imported object: <a href="{}">{}</a>'.format(
+                    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 return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
+                    return redirect(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, "{}: {}".format(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),
+        })
+
+
+class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
+    """
+    Import objects in bulk (CSV format).
+
+    queryset: Base queryset for the model
+    model_form: The form used to create each imported object
+    table: The django-tables2 Table used to render the list of imported objects
+    template_name: The name of the template
+    widget_attrs: A dict of attributes to apply to the import widget (e.g. to require a session key)
+    """
+    queryset = None
+    model_form = None
+    table = None
+    template_name = 'utilities/obj_bulk_import.html'
+    widget_attrs = {}
+
+    def _import_form(self, *args, **kwargs):
+
+        class ImportForm(BootstrapMixin, Form):
+            csv = CSVDataField(
+                from_form=self.model_form,
+                widget=Textarea(attrs=self.widget_attrs)
+            )
+
+        return ImportForm(*args, **kwargs)
+
+    def _save_obj(self, obj_form, request):
+        """
+        Provide a hook to modify the object immediately before saving it (e.g. to encrypt secret data).
+        """
+        return obj_form.save()
+
+    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.model_form().fields,
+            'obj_type': self.model_form._meta.model._meta.verbose_name,
+            'return_url': self.get_return_url(request),
+        })
+
+    def post(self, request):
+        logger = logging.getLogger('netbox.views.BulkImportView')
+        new_objs = []
+        form = self._import_form(request.POST)
+
+        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():
+                    headers, records = form.cleaned_data['csv']
+                    for row, data in enumerate(records, start=1):
+                        obj_form = self.model_form(data, headers=headers)
+                        restrict_form_fields(obj_form, request.user)
+
+                        if obj_form.is_valid():
+                            obj = self._save_obj(obj_form, request)
+                            new_objs.append(obj)
+                        else:
+                            for field, err in obj_form.errors.items():
+                                form.add_error('csv', "Row {} {}: {}".format(row, field, err[0]))
+                            raise ValidationError("")
+
+                    # 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 = 'Imported {} {}'.format(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.model_form().fields,
+            'obj_type': self.model_form._meta.model._meta.verbose_name,
+            'return_url': self.get_return_url(request),
+        })
+
+
+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 = 'utilities/obj_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 post(self, request, **kwargs):
+        logger = logging.getLogger('netbox.views.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') and self.filterset is not None:
+            pk_list = [
+                obj.pk for obj in self.filterset(request.GET, self.queryset.only('pk')).qs
+            ]
+        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")
+                custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
+                standard_fields = [
+                    field for field in form.fields if field not in custom_fields + ['pk']
+                ]
+                nullified_fields = request.POST.getlist('_nullify')
+
+                try:
+
+                    with transaction.atomic():
+
+                        updated_objects = []
+                        for obj in self.queryset.filter(pk__in=form.cleaned_data['pk']):
+
+                            # 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.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 name in custom_fields:
+                                if name in form.nullable_fields and name in nullified_fields:
+                                    obj.custom_field_data.pop(name, None)
+                                else:
+                                    obj.custom_field_data[name] = form.cleaned_data[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'])
+
+                        # 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 = 'Updated {} {}'.format(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, "{} failed validation: {}".format(obj, 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, "No {} were selected.".format(model._meta.verbose_name_plural))
+            return redirect(self.get_return_url(request))
+
+        return render(request, self.template_name, {
+            'form': form,
+            'table': table,
+            'obj_type_plural': model._meta.verbose_name_plural,
+            'return_url': self.get_return_url(request),
+        })
+
+
+class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
+    """
+    An extendable view for renaming objects in bulk.
+    """
+    queryset = None
+    template_name = 'utilities/obj_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('netbox.views.BulkRenameView')
+
+        if '_preview' in request.POST or '_apply' in request.POST:
+            form = self.form(request.POST, initial={'pk': request.POST.getlist('pk')})
+            selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
+
+            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, "Renamed {} {}".format(
+                                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': request.POST.getlist('pk')})
+            selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
+
+        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),
+        })
+
+
+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 = 'utilities/obj_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('netbox.views.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 = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs]
+            else:
+                pk_list = model.objects.values_list('pk', flat=True)
+        else:
+            pk_list = [int(pk) for pk in 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)
+                try:
+                    deleted_count = queryset.delete()[1][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 = 'Deleted {} {}'.format(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, "No {} were selected for deletion.".format(model._meta.verbose_name_plural))
+            return redirect(self.get_return_url(request))
+
+        return render(request, self.template_name, {
+            'form': form,
+            'obj_type_plural': model._meta.verbose_name_plural,
+            'table': table,
+            'return_url': self.get_return_url(request),
+        })
+
+    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
+
+
+#
+# Device/VirtualMachine components
+#
+
+# TODO: Replace with BulkCreateView
+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)
+
+        return render(request, self.template_name, {
+            'component_type': self.queryset.model._meta.verbose_name,
+            'form': form,
+            'return_url': self.get_return_url(request),
+        })
+
+    def post(self, request):
+        logger = logging.getLogger('netbox.views.ComponentCreateView')
+        form = self.form(request.POST, initial=request.GET)
+
+        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:
+                            form.add_error(field, '{}: {}'.format(name, ', '.join(e)))
+
+            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, "Added {} {}".format(
+                        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,
+            'return_url': self.get_return_url(request),
+        })
+
+
+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 = 'utilities/obj_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('netbox.views.BulkComponentCreateView')
+        parent_model_name = self.parent_model._meta.verbose_name_plural
+        model_name = self.queryset.model._meta.verbose_name_plural
+
+        # 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 = [int(pk) for pk in request.POST.getlist('pk')]
+
+        selected_objects = self.parent_model.objects.filter(pk__in=pk_list)
+        if not selected_objects:
+            messages.warning(request, "No {} were selected.".format(self.parent_model._meta.verbose_name_plural))
+            return redirect(self.get_return_url(request))
+        table = self.table(selected_objects)
+
+        if '_create' in request.POST:
+            form = self.form(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:
+                                            form.add_error(field, '{} {}: {}'.format(obj, name, ', '.join(e)))
+
+                        # 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 = "Added {} {} to {} {}.".format(
+                        len(new_components),
+                        model_name,
+                        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(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),
+        })

+ 13 - 15
netbox/secrets/views.py

@@ -7,9 +7,7 @@ from django.shortcuts import get_object_or_404, redirect, render
 from django.utils.html import escape
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 
 
-from utilities.views import (
-    BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
-)
+from netbox.views import generic
 from . import filters, forms, tables
 from . import filters, forms, tables
 from .models import SecretRole, Secret, SessionKey, UserKey
 from .models import SecretRole, Secret, SessionKey, UserKey
 
 
@@ -28,27 +26,27 @@ def get_session_key(request):
 # Secret roles
 # Secret roles
 #
 #
 
 
-class SecretRoleListView(ObjectListView):
+class SecretRoleListView(generic.ObjectListView):
     queryset = SecretRole.objects.annotate(secret_count=Count('secrets')).order_by(*SecretRole._meta.ordering)
     queryset = SecretRole.objects.annotate(secret_count=Count('secrets')).order_by(*SecretRole._meta.ordering)
     table = tables.SecretRoleTable
     table = tables.SecretRoleTable
 
 
 
 
-class SecretRoleEditView(ObjectEditView):
+class SecretRoleEditView(generic.ObjectEditView):
     queryset = SecretRole.objects.all()
     queryset = SecretRole.objects.all()
     model_form = forms.SecretRoleForm
     model_form = forms.SecretRoleForm
 
 
 
 
-class SecretRoleDeleteView(ObjectDeleteView):
+class SecretRoleDeleteView(generic.ObjectDeleteView):
     queryset = SecretRole.objects.all()
     queryset = SecretRole.objects.all()
 
 
 
 
-class SecretRoleBulkImportView(BulkImportView):
+class SecretRoleBulkImportView(generic.BulkImportView):
     queryset = SecretRole.objects.all()
     queryset = SecretRole.objects.all()
     model_form = forms.SecretRoleCSVForm
     model_form = forms.SecretRoleCSVForm
     table = tables.SecretRoleTable
     table = tables.SecretRoleTable
 
 
 
 
-class SecretRoleBulkDeleteView(BulkDeleteView):
+class SecretRoleBulkDeleteView(generic.BulkDeleteView):
     queryset = SecretRole.objects.annotate(secret_count=Count('secrets')).order_by(*SecretRole._meta.ordering)
     queryset = SecretRole.objects.annotate(secret_count=Count('secrets')).order_by(*SecretRole._meta.ordering)
     table = tables.SecretRoleTable
     table = tables.SecretRoleTable
 
 
@@ -57,7 +55,7 @@ class SecretRoleBulkDeleteView(BulkDeleteView):
 # Secrets
 # Secrets
 #
 #
 
 
-class SecretListView(ObjectListView):
+class SecretListView(generic.ObjectListView):
     queryset = Secret.objects.all()
     queryset = Secret.objects.all()
     filterset = filters.SecretFilterSet
     filterset = filters.SecretFilterSet
     filterset_form = forms.SecretFilterForm
     filterset_form = forms.SecretFilterForm
@@ -65,7 +63,7 @@ class SecretListView(ObjectListView):
     action_buttons = ('import', 'export')
     action_buttons = ('import', 'export')
 
 
 
 
-class SecretView(ObjectView):
+class SecretView(generic.ObjectView):
     queryset = Secret.objects.all()
     queryset = Secret.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -77,7 +75,7 @@ class SecretView(ObjectView):
         })
         })
 
 
 
 
-class SecretEditView(ObjectEditView):
+class SecretEditView(generic.ObjectEditView):
     queryset = Secret.objects.all()
     queryset = Secret.objects.all()
     model_form = forms.SecretForm
     model_form = forms.SecretForm
     template_name = 'secrets/secret_edit.html'
     template_name = 'secrets/secret_edit.html'
@@ -146,11 +144,11 @@ class SecretEditView(ObjectEditView):
         })
         })
 
 
 
 
-class SecretDeleteView(ObjectDeleteView):
+class SecretDeleteView(generic.ObjectDeleteView):
     queryset = Secret.objects.all()
     queryset = Secret.objects.all()
 
 
 
 
-class SecretBulkImportView(BulkImportView):
+class SecretBulkImportView(generic.BulkImportView):
     queryset = Secret.objects.all()
     queryset = Secret.objects.all()
     model_form = forms.SecretCSVForm
     model_form = forms.SecretCSVForm
     table = tables.SecretTable
     table = tables.SecretTable
@@ -197,14 +195,14 @@ class SecretBulkImportView(BulkImportView):
         })
         })
 
 
 
 
-class SecretBulkEditView(BulkEditView):
+class SecretBulkEditView(generic.BulkEditView):
     queryset = Secret.objects.prefetch_related('role')
     queryset = Secret.objects.prefetch_related('role')
     filterset = filters.SecretFilterSet
     filterset = filters.SecretFilterSet
     table = tables.SecretTable
     table = tables.SecretTable
     form = forms.SecretBulkEditForm
     form = forms.SecretBulkEditForm
 
 
 
 
-class SecretBulkDeleteView(BulkDeleteView):
+class SecretBulkDeleteView(generic.BulkDeleteView):
     queryset = Secret.objects.prefetch_related('role')
     queryset = Secret.objects.prefetch_related('role')
     filterset = filters.SecretFilterSet
     filterset = filters.SecretFilterSet
     table = tables.SecretTable
     table = tables.SecretTable

+ 13 - 16
netbox/tenancy/views.py

@@ -1,12 +1,9 @@
-from django.db.models import Count
 from django.shortcuts import get_object_or_404, render
 from django.shortcuts import get_object_or_404, render
 
 
 from circuits.models import Circuit
 from circuits.models import Circuit
 from dcim.models import Site, Rack, Device, RackReservation
 from dcim.models import Site, Rack, Device, RackReservation
 from ipam.models import IPAddress, Prefix, VLAN, VRF
 from ipam.models import IPAddress, Prefix, VLAN, VRF
-from utilities.views import (
-    BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
-)
+from netbox.views import generic
 from virtualization.models import VirtualMachine, Cluster
 from virtualization.models import VirtualMachine, Cluster
 from . import filters, forms, tables
 from . import filters, forms, tables
 from .models import Tenant, TenantGroup
 from .models import Tenant, TenantGroup
@@ -16,7 +13,7 @@ from .models import Tenant, TenantGroup
 # Tenant groups
 # Tenant groups
 #
 #
 
 
-class TenantGroupListView(ObjectListView):
+class TenantGroupListView(generic.ObjectListView):
     queryset = TenantGroup.objects.add_related_count(
     queryset = TenantGroup.objects.add_related_count(
         TenantGroup.objects.all(),
         TenantGroup.objects.all(),
         Tenant,
         Tenant,
@@ -27,22 +24,22 @@ class TenantGroupListView(ObjectListView):
     table = tables.TenantGroupTable
     table = tables.TenantGroupTable
 
 
 
 
-class TenantGroupEditView(ObjectEditView):
+class TenantGroupEditView(generic.ObjectEditView):
     queryset = TenantGroup.objects.all()
     queryset = TenantGroup.objects.all()
     model_form = forms.TenantGroupForm
     model_form = forms.TenantGroupForm
 
 
 
 
-class TenantGroupDeleteView(ObjectDeleteView):
+class TenantGroupDeleteView(generic.ObjectDeleteView):
     queryset = TenantGroup.objects.all()
     queryset = TenantGroup.objects.all()
 
 
 
 
-class TenantGroupBulkImportView(BulkImportView):
+class TenantGroupBulkImportView(generic.BulkImportView):
     queryset = TenantGroup.objects.all()
     queryset = TenantGroup.objects.all()
     model_form = forms.TenantGroupCSVForm
     model_form = forms.TenantGroupCSVForm
     table = tables.TenantGroupTable
     table = tables.TenantGroupTable
 
 
 
 
-class TenantGroupBulkDeleteView(BulkDeleteView):
+class TenantGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = TenantGroup.objects.add_related_count(
     queryset = TenantGroup.objects.add_related_count(
         TenantGroup.objects.all(),
         TenantGroup.objects.all(),
         Tenant,
         Tenant,
@@ -57,14 +54,14 @@ class TenantGroupBulkDeleteView(BulkDeleteView):
 #  Tenants
 #  Tenants
 #
 #
 
 
-class TenantListView(ObjectListView):
+class TenantListView(generic.ObjectListView):
     queryset = Tenant.objects.all()
     queryset = Tenant.objects.all()
     filterset = filters.TenantFilterSet
     filterset = filters.TenantFilterSet
     filterset_form = forms.TenantFilterForm
     filterset_form = forms.TenantFilterForm
     table = tables.TenantTable
     table = tables.TenantTable
 
 
 
 
-class TenantView(ObjectView):
+class TenantView(generic.ObjectView):
     queryset = Tenant.objects.prefetch_related('group')
     queryset = Tenant.objects.prefetch_related('group')
 
 
     def get(self, request, slug):
     def get(self, request, slug):
@@ -90,30 +87,30 @@ class TenantView(ObjectView):
         })
         })
 
 
 
 
-class TenantEditView(ObjectEditView):
+class TenantEditView(generic.ObjectEditView):
     queryset = Tenant.objects.all()
     queryset = Tenant.objects.all()
     model_form = forms.TenantForm
     model_form = forms.TenantForm
     template_name = 'tenancy/tenant_edit.html'
     template_name = 'tenancy/tenant_edit.html'
 
 
 
 
-class TenantDeleteView(ObjectDeleteView):
+class TenantDeleteView(generic.ObjectDeleteView):
     queryset = Tenant.objects.all()
     queryset = Tenant.objects.all()
 
 
 
 
-class TenantBulkImportView(BulkImportView):
+class TenantBulkImportView(generic.BulkImportView):
     queryset = Tenant.objects.all()
     queryset = Tenant.objects.all()
     model_form = forms.TenantCSVForm
     model_form = forms.TenantCSVForm
     table = tables.TenantTable
     table = tables.TenantTable
 
 
 
 
-class TenantBulkEditView(BulkEditView):
+class TenantBulkEditView(generic.BulkEditView):
     queryset = Tenant.objects.prefetch_related('group')
     queryset = Tenant.objects.prefetch_related('group')
     filterset = filters.TenantFilterSet
     filterset = filters.TenantFilterSet
     table = tables.TenantTable
     table = tables.TenantTable
     form = forms.TenantBulkEditForm
     form = forms.TenantBulkEditForm
 
 
 
 
-class TenantBulkDeleteView(BulkDeleteView):
+class TenantBulkDeleteView(generic.BulkDeleteView):
     queryset = Tenant.objects.prefetch_related('group')
     queryset = Tenant.objects.prefetch_related('group')
     filterset = filters.TenantFilterSet
     filterset = filters.TenantFilterSet
     table = tables.TenantTable
     table = tables.TenantTable

+ 3 - 1250
netbox/utilities/views.py

@@ -1,43 +1,14 @@
-import logging
-import platform
-import re
-import sys
-from copy import deepcopy
-
-from django.conf import settings
-from django.contrib import messages
 from django.contrib.auth.mixins import AccessMixin
 from django.contrib.auth.mixins import AccessMixin
-from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured, ObjectDoesNotExist, ValidationError
-from django.db import transaction, IntegrityError
-from django.db.models import ManyToManyField, ProtectedError
-from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
-from django.http import HttpResponse, HttpResponseServerError
-from django.shortcuts import get_object_or_404, redirect, render
-from django.template import loader
-from django.template.exceptions import TemplateDoesNotExist
+from django.core.exceptions import ImproperlyConfigured
 from django.urls import reverse
 from django.urls import reverse
 from django.urls.exceptions import NoReverseMatch
 from django.urls.exceptions import NoReverseMatch
-from django.utils.html import escape
 from django.utils.http import is_safe_url
 from django.utils.http import is_safe_url
-from django.utils.safestring import mark_safe
-from django.views.decorators.csrf import requires_csrf_token
-from django.views.defaults import ERROR_500_TEMPLATE_NAME
-from django.views.generic import View
-from django_tables2 import RequestConfig
 
 
-from extras.models import CustomField, ExportTemplate
-from utilities.exceptions import AbortTransaction
-from utilities.forms import BootstrapMixin, BulkRenameForm, CSVDataField, TableConfigForm, restrict_form_fields
-from utilities.permissions import get_permission_for_model, resolve_permission
-from utilities.utils import csv_format, normalize_querydict, prepare_cloned_fields
-from .error_handlers import handle_protectederror
-from .forms import ConfirmationForm, ImportForm
-from .paginator import EnhancedPaginator, get_paginate_count
+from .permissions import resolve_permission
 
 
 
 
 #
 #
-# Mixins
+# View Mixins
 #
 #
 
 
 class ContentTypePermissionRequiredMixin(AccessMixin):
 class ContentTypePermissionRequiredMixin(AccessMixin):
@@ -152,1221 +123,3 @@ class GetReturnURLMixin:
 
 
         # If all else fails, return home. Ideally this should never happen.
         # If all else fails, return home. Ideally this should never happen.
         return reverse('home')
         return reverse('home')
-
-
-#
-# Generic views
-#
-
-class ObjectView(ObjectPermissionRequiredMixin, View):
-    """
-    Retrieve a single object for display.
-
-    queryset: The base queryset for retrieving the object.
-    """
-    queryset = 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 hasattr(self, 'template_name'):
-            return self.template_name
-        model_opts = self.queryset.model._meta
-        return f'{model_opts.app_label}/{model_opts.model_name}.html'
-
-    def get(self, request, pk):
-        """
-        Generic GET handler for accessing an object by PK
-        """
-        instance = get_object_or_404(self.queryset, pk=pk)
-
-        return render(request, self.get_template_name(), {
-            'instance': instance,
-        })
-
-
-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
-    """
-    queryset = None
-    filterset = None
-    filterset_form = None
-    table = None
-    template_name = 'utilities/obj_list.html'
-    action_buttons = ('add', 'import', 'export')
-
-    def get_required_permission(self):
-        return get_permission_for_model(self.queryset.model, 'view')
-
-    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 queryset_to_csv(self):
-        """
-        Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method.
-        """
-        csv_data = []
-        custom_fields = []
-
-        # Start with the column headers
-        headers = self.queryset.model.csv_headers.copy()
-
-        # Add custom field headers, if any
-        if hasattr(self.queryset.model, 'custom_field_data'):
-            for custom_field in CustomField.objects.get_for_model(self.queryset.model):
-                headers.append(custom_field.name)
-                custom_fields.append(custom_field.name)
-
-        csv_data.append(','.join(headers))
-
-        # Iterate through the queryset appending each object
-        for obj in self.queryset:
-            data = obj.to_csv()
-
-            for custom_field in custom_fields:
-                data += (obj.cf.get(custom_field, ''),)
-
-            csv_data.append(csv_format(data))
-
-        return '\n'.join(csv_data)
-
-    def get(self, request):
-
-        model = self.queryset.model
-        content_type = ContentType.objects.get_for_model(model)
-
-        if self.filterset:
-            self.queryset = self.filterset(request.GET, self.queryset).qs
-
-        # Check for export template rendering
-        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(self.queryset)
-            except Exception as e:
-                messages.error(
-                    request,
-                    "There was an error rendering the selected export template ({}): {}".format(
-                        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 = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural)
-            response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
-            return response
-
-        # Fall back to built-in CSV formatting if export requested but no template specified
-        elif 'export' in request.GET and hasattr(model, 'to_csv'):
-            response = HttpResponse(self.queryset_to_csv(), content_type='text/csv')
-            filename = 'netbox_{}.csv'.format(self.queryset.model._meta.verbose_name_plural)
-            response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
-            return response
-
-        # 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)
-
-        # Construct the objects table
-        table = self.table(self.queryset, user=request.user)
-        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)
-
-        context = {
-            'content_type': content_type,
-            'table': table,
-            'permissions': permissions,
-            'action_buttons': self.action_buttons,
-            'table_config_form': TableConfigForm(table=table),
-            'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
-        }
-        context.update(self.extra_context())
-
-        return render(request, self.template_name, context)
-
-    def extra_context(self):
-        return {}
-
-
-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 = 'utilities/obj_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):
-        # Look up an existing object by slug or PK, if provided.
-        if 'slug' in kwargs:
-            return get_object_or_404(self.queryset, slug=kwargs['slug'])
-        elif 'pk' in kwargs:
-            return get_object_or_404(self.queryset, pk=kwargs['pk'])
-        # Otherwise, return a new instance.
-        return self.queryset.model()
-
-    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 = 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),
-        })
-
-    def post(self, request, *args, **kwargs):
-        logger = logging.getLogger('netbox.views.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 = form.instance.pk is None
-                    obj = form.save()
-
-                    # Check that the new object conforms with any assigned object-level permissions
-                    self.queryset.get(pk=obj.pk)
-
-                msg = '{} {}'.format(
-                    'Created' if object_created else 'Modified',
-                    self.queryset.model._meta.verbose_name
-                )
-                logger.info(f"{msg} {obj} (PK: {obj.pk})")
-                if hasattr(obj, 'get_absolute_url'):
-                    msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
-                else:
-                    msg = '{} {}'.format(msg, escape(obj))
-                messages.success(request, mark_safe(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 = '{}?{}'.format(request.path, prepare_cloned_fields(obj))
-                        return redirect(url)
-
-                    return redirect(request.get_full_path())
-
-                return_url = form.cleaned_data.get('return_url')
-                if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
-                    return redirect(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),
-        })
-
-
-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 = 'utilities/obj_delete.html'
-
-    def get_required_permission(self):
-        return get_permission_for_model(self.queryset.model, 'delete')
-
-    def get_object(self, kwargs):
-        # Look up object by slug if one has been provided. Otherwise, use PK.
-        if 'slug' in kwargs:
-            return get_object_or_404(self.queryset, slug=kwargs['slug'])
-        else:
-            return get_object_or_404(self.queryset, pk=kwargs['pk'])
-
-    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('netbox.views.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 = 'Deleted {} {}'.format(self.queryset.model._meta.verbose_name, obj)
-            logger.info(msg)
-            messages.success(request, msg)
-
-            return_url = form.cleaned_data.get('return_url')
-            if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
-                return redirect(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),
-        })
-
-
-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('netbox.views.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 = "Added {} {}".format(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),
-        })
-
-
-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 = dict()
-    template_name = 'utilities/obj_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('netbox.views.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, list())):
-
-                                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 = "{}[{}] {}: {}".format(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, mark_safe('Imported object: <a href="{}">{}</a>'.format(
-                    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 return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
-                    return redirect(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, "{}: {}".format(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),
-        })
-
-
-class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
-    """
-    Import objects in bulk (CSV format).
-
-    queryset: Base queryset for the model
-    model_form: The form used to create each imported object
-    table: The django-tables2 Table used to render the list of imported objects
-    template_name: The name of the template
-    widget_attrs: A dict of attributes to apply to the import widget (e.g. to require a session key)
-    """
-    queryset = None
-    model_form = None
-    table = None
-    template_name = 'utilities/obj_bulk_import.html'
-    widget_attrs = {}
-
-    def _import_form(self, *args, **kwargs):
-
-        class ImportForm(BootstrapMixin, Form):
-            csv = CSVDataField(
-                from_form=self.model_form,
-                widget=Textarea(attrs=self.widget_attrs)
-            )
-
-        return ImportForm(*args, **kwargs)
-
-    def _save_obj(self, obj_form, request):
-        """
-        Provide a hook to modify the object immediately before saving it (e.g. to encrypt secret data).
-        """
-        return obj_form.save()
-
-    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.model_form().fields,
-            'obj_type': self.model_form._meta.model._meta.verbose_name,
-            'return_url': self.get_return_url(request),
-        })
-
-    def post(self, request):
-        logger = logging.getLogger('netbox.views.BulkImportView')
-        new_objs = []
-        form = self._import_form(request.POST)
-
-        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():
-                    headers, records = form.cleaned_data['csv']
-                    for row, data in enumerate(records, start=1):
-                        obj_form = self.model_form(data, headers=headers)
-                        restrict_form_fields(obj_form, request.user)
-
-                        if obj_form.is_valid():
-                            obj = self._save_obj(obj_form, request)
-                            new_objs.append(obj)
-                        else:
-                            for field, err in obj_form.errors.items():
-                                form.add_error('csv', "Row {} {}: {}".format(row, field, err[0]))
-                            raise ValidationError("")
-
-                    # 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 = 'Imported {} {}'.format(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.model_form().fields,
-            'obj_type': self.model_form._meta.model._meta.verbose_name,
-            'return_url': self.get_return_url(request),
-        })
-
-
-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 = 'utilities/obj_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 post(self, request, **kwargs):
-        logger = logging.getLogger('netbox.views.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') and self.filterset is not None:
-            pk_list = [
-                obj.pk for obj in self.filterset(request.GET, self.queryset.only('pk')).qs
-            ]
-        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")
-                custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
-                standard_fields = [
-                    field for field in form.fields if field not in custom_fields + ['pk']
-                ]
-                nullified_fields = request.POST.getlist('_nullify')
-
-                try:
-
-                    with transaction.atomic():
-
-                        updated_objects = []
-                        for obj in self.queryset.filter(pk__in=form.cleaned_data['pk']):
-
-                            # 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.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 name in custom_fields:
-                                if name in form.nullable_fields and name in nullified_fields:
-                                    obj.custom_field_data.pop(name, None)
-                                else:
-                                    obj.custom_field_data[name] = form.cleaned_data[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'])
-
-                        # 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 = 'Updated {} {}'.format(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, "{} failed validation: {}".format(obj, 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, "No {} were selected.".format(model._meta.verbose_name_plural))
-            return redirect(self.get_return_url(request))
-
-        return render(request, self.template_name, {
-            'form': form,
-            'table': table,
-            'obj_type_plural': model._meta.verbose_name_plural,
-            'return_url': self.get_return_url(request),
-        })
-
-
-class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
-    """
-    An extendable view for renaming objects in bulk.
-    """
-    queryset = None
-    template_name = 'utilities/obj_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('netbox.views.BulkRenameView')
-
-        if '_preview' in request.POST or '_apply' in request.POST:
-            form = self.form(request.POST, initial={'pk': request.POST.getlist('pk')})
-            selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
-
-            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, "Renamed {} {}".format(
-                                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': request.POST.getlist('pk')})
-            selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
-
-        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),
-        })
-
-
-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 = 'utilities/obj_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('netbox.views.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 = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs]
-            else:
-                pk_list = model.objects.values_list('pk', flat=True)
-        else:
-            pk_list = [int(pk) for pk in 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)
-                try:
-                    deleted_count = queryset.delete()[1][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 = 'Deleted {} {}'.format(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, "No {} were selected for deletion.".format(model._meta.verbose_name_plural))
-            return redirect(self.get_return_url(request))
-
-        return render(request, self.template_name, {
-            'form': form,
-            'obj_type_plural': model._meta.verbose_name_plural,
-            'table': table,
-            'return_url': self.get_return_url(request),
-        })
-
-    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
-
-
-#
-# Device/VirtualMachine components
-#
-
-# TODO: Replace with BulkCreateView
-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)
-
-        return render(request, self.template_name, {
-            'component_type': self.queryset.model._meta.verbose_name,
-            'form': form,
-            'return_url': self.get_return_url(request),
-        })
-
-    def post(self, request):
-        logger = logging.getLogger('netbox.views.ComponentCreateView')
-        form = self.form(request.POST, initial=request.GET)
-
-        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:
-                            form.add_error(field, '{}: {}'.format(name, ', '.join(e)))
-
-            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, "Added {} {}".format(
-                        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,
-            'return_url': self.get_return_url(request),
-        })
-
-
-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 = 'utilities/obj_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('netbox.views.BulkComponentCreateView')
-        parent_model_name = self.parent_model._meta.verbose_name_plural
-        model_name = self.queryset.model._meta.verbose_name_plural
-
-        # 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 = [int(pk) for pk in request.POST.getlist('pk')]
-
-        selected_objects = self.parent_model.objects.filter(pk__in=pk_list)
-        if not selected_objects:
-            messages.warning(request, "No {} were selected.".format(self.parent_model._meta.verbose_name_plural))
-            return redirect(self.get_return_url(request))
-        table = self.table(selected_objects)
-
-        if '_create' in request.POST:
-            form = self.form(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:
-                                            form.add_error(field, '{} {}: {}'.format(obj, name, ', '.join(e)))
-
-                        # 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 = "Added {} {} to {} {}.".format(
-                        len(new_components),
-                        model_name,
-                        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(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),
-        })
-
-
-@requires_csrf_token
-def server_error(request, template_name=ERROR_500_TEMPLATE_NAME):
-    """
-    Custom 500 handler to provide additional context when rendering 500.html.
-    """
-    try:
-        template = loader.get_template(template_name)
-    except TemplateDoesNotExist:
-        return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
-    type_, error, traceback = sys.exc_info()
-
-    return HttpResponseServerError(template.render({
-        'error': error,
-        'exception': str(type_),
-        'netbox_version': settings.VERSION,
-        'python_version': platform.python_version(),
-    }))

+ 37 - 40
netbox/virtualization/views.py

@@ -9,12 +9,9 @@ from dcim.tables import DeviceTable
 from extras.views import ObjectConfigContextView
 from extras.views import ObjectConfigContextView
 from ipam.models import IPAddress, Service
 from ipam.models import IPAddress, Service
 from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
 from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
+from netbox.views import generic
 from secrets.models import Secret
 from secrets.models import Secret
 from utilities.utils import get_subquery
 from utilities.utils import get_subquery
-from utilities.views import (
-    BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, BulkRenameView, ComponentCreateView,
-    ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
-)
 from . import filters, forms, tables
 from . import filters, forms, tables
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 
 
@@ -23,27 +20,27 @@ from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterf
 # Cluster types
 # Cluster types
 #
 #
 
 
-class ClusterTypeListView(ObjectListView):
+class ClusterTypeListView(generic.ObjectListView):
     queryset = ClusterType.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterType._meta.ordering)
     queryset = ClusterType.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterType._meta.ordering)
     table = tables.ClusterTypeTable
     table = tables.ClusterTypeTable
 
 
 
 
-class ClusterTypeEditView(ObjectEditView):
+class ClusterTypeEditView(generic.ObjectEditView):
     queryset = ClusterType.objects.all()
     queryset = ClusterType.objects.all()
     model_form = forms.ClusterTypeForm
     model_form = forms.ClusterTypeForm
 
 
 
 
-class ClusterTypeDeleteView(ObjectDeleteView):
+class ClusterTypeDeleteView(generic.ObjectDeleteView):
     queryset = ClusterType.objects.all()
     queryset = ClusterType.objects.all()
 
 
 
 
-class ClusterTypeBulkImportView(BulkImportView):
+class ClusterTypeBulkImportView(generic.BulkImportView):
     queryset = ClusterType.objects.all()
     queryset = ClusterType.objects.all()
     model_form = forms.ClusterTypeCSVForm
     model_form = forms.ClusterTypeCSVForm
     table = tables.ClusterTypeTable
     table = tables.ClusterTypeTable
 
 
 
 
-class ClusterTypeBulkDeleteView(BulkDeleteView):
+class ClusterTypeBulkDeleteView(generic.BulkDeleteView):
     queryset = ClusterType.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterType._meta.ordering)
     queryset = ClusterType.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterType._meta.ordering)
     table = tables.ClusterTypeTable
     table = tables.ClusterTypeTable
 
 
@@ -52,27 +49,27 @@ class ClusterTypeBulkDeleteView(BulkDeleteView):
 # Cluster groups
 # Cluster groups
 #
 #
 
 
-class ClusterGroupListView(ObjectListView):
+class ClusterGroupListView(generic.ObjectListView):
     queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterGroup._meta.ordering)
     queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterGroup._meta.ordering)
     table = tables.ClusterGroupTable
     table = tables.ClusterGroupTable
 
 
 
 
-class ClusterGroupEditView(ObjectEditView):
+class ClusterGroupEditView(generic.ObjectEditView):
     queryset = ClusterGroup.objects.all()
     queryset = ClusterGroup.objects.all()
     model_form = forms.ClusterGroupForm
     model_form = forms.ClusterGroupForm
 
 
 
 
-class ClusterGroupDeleteView(ObjectDeleteView):
+class ClusterGroupDeleteView(generic.ObjectDeleteView):
     queryset = ClusterGroup.objects.all()
     queryset = ClusterGroup.objects.all()
 
 
 
 
-class ClusterGroupBulkImportView(BulkImportView):
+class ClusterGroupBulkImportView(generic.BulkImportView):
     queryset = ClusterGroup.objects.all()
     queryset = ClusterGroup.objects.all()
     model_form = forms.ClusterGroupCSVForm
     model_form = forms.ClusterGroupCSVForm
     table = tables.ClusterGroupTable
     table = tables.ClusterGroupTable
 
 
 
 
-class ClusterGroupBulkDeleteView(BulkDeleteView):
+class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterGroup._meta.ordering)
     queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterGroup._meta.ordering)
     table = tables.ClusterGroupTable
     table = tables.ClusterGroupTable
 
 
@@ -81,7 +78,7 @@ class ClusterGroupBulkDeleteView(BulkDeleteView):
 # Clusters
 # Clusters
 #
 #
 
 
-class ClusterListView(ObjectListView):
+class ClusterListView(generic.ObjectListView):
     permission_required = 'virtualization.view_cluster'
     permission_required = 'virtualization.view_cluster'
     queryset = Cluster.objects.annotate(
     queryset = Cluster.objects.annotate(
         device_count=get_subquery(Device, 'cluster'),
         device_count=get_subquery(Device, 'cluster'),
@@ -92,7 +89,7 @@ class ClusterListView(ObjectListView):
     filterset_form = forms.ClusterFilterForm
     filterset_form = forms.ClusterFilterForm
 
 
 
 
-class ClusterView(ObjectView):
+class ClusterView(generic.ObjectView):
     queryset = Cluster.objects.all()
     queryset = Cluster.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -114,36 +111,36 @@ class ClusterView(ObjectView):
         })
         })
 
 
 
 
-class ClusterEditView(ObjectEditView):
+class ClusterEditView(generic.ObjectEditView):
     template_name = 'virtualization/cluster_edit.html'
     template_name = 'virtualization/cluster_edit.html'
     queryset = Cluster.objects.all()
     queryset = Cluster.objects.all()
     model_form = forms.ClusterForm
     model_form = forms.ClusterForm
 
 
 
 
-class ClusterDeleteView(ObjectDeleteView):
+class ClusterDeleteView(generic.ObjectDeleteView):
     queryset = Cluster.objects.all()
     queryset = Cluster.objects.all()
 
 
 
 
-class ClusterBulkImportView(BulkImportView):
+class ClusterBulkImportView(generic.BulkImportView):
     queryset = Cluster.objects.all()
     queryset = Cluster.objects.all()
     model_form = forms.ClusterCSVForm
     model_form = forms.ClusterCSVForm
     table = tables.ClusterTable
     table = tables.ClusterTable
 
 
 
 
-class ClusterBulkEditView(BulkEditView):
+class ClusterBulkEditView(generic.BulkEditView):
     queryset = Cluster.objects.prefetch_related('type', 'group', 'site')
     queryset = Cluster.objects.prefetch_related('type', 'group', 'site')
     filterset = filters.ClusterFilterSet
     filterset = filters.ClusterFilterSet
     table = tables.ClusterTable
     table = tables.ClusterTable
     form = forms.ClusterBulkEditForm
     form = forms.ClusterBulkEditForm
 
 
 
 
-class ClusterBulkDeleteView(BulkDeleteView):
+class ClusterBulkDeleteView(generic.BulkDeleteView):
     queryset = Cluster.objects.prefetch_related('type', 'group', 'site')
     queryset = Cluster.objects.prefetch_related('type', 'group', 'site')
     filterset = filters.ClusterFilterSet
     filterset = filters.ClusterFilterSet
     table = tables.ClusterTable
     table = tables.ClusterTable
 
 
 
 
-class ClusterAddDevicesView(ObjectEditView):
+class ClusterAddDevicesView(generic.ObjectEditView):
     queryset = Cluster.objects.all()
     queryset = Cluster.objects.all()
     form = forms.ClusterAddDevicesForm
     form = forms.ClusterAddDevicesForm
     template_name = 'virtualization/cluster_add_devices.html'
     template_name = 'virtualization/cluster_add_devices.html'
@@ -184,7 +181,7 @@ class ClusterAddDevicesView(ObjectEditView):
         })
         })
 
 
 
 
-class ClusterRemoveDevicesView(ObjectEditView):
+class ClusterRemoveDevicesView(generic.ObjectEditView):
     queryset = Cluster.objects.all()
     queryset = Cluster.objects.all()
     form = forms.ClusterRemoveDevicesForm
     form = forms.ClusterRemoveDevicesForm
     template_name = 'utilities/obj_bulk_remove.html'
     template_name = 'utilities/obj_bulk_remove.html'
@@ -229,7 +226,7 @@ class ClusterRemoveDevicesView(ObjectEditView):
 # Virtual machines
 # Virtual machines
 #
 #
 
 
-class VirtualMachineListView(ObjectListView):
+class VirtualMachineListView(generic.ObjectListView):
     queryset = VirtualMachine.objects.all()
     queryset = VirtualMachine.objects.all()
     filterset = filters.VirtualMachineFilterSet
     filterset = filters.VirtualMachineFilterSet
     filterset_form = forms.VirtualMachineFilterForm
     filterset_form = forms.VirtualMachineFilterForm
@@ -237,7 +234,7 @@ class VirtualMachineListView(ObjectListView):
     template_name = 'virtualization/virtualmachine_list.html'
     template_name = 'virtualization/virtualmachine_list.html'
 
 
 
 
-class VirtualMachineView(ObjectView):
+class VirtualMachineView(generic.ObjectView):
     queryset = VirtualMachine.objects.prefetch_related('tenant__group')
     queryset = VirtualMachine.objects.prefetch_related('tenant__group')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -277,30 +274,30 @@ class VirtualMachineConfigContextView(ObjectConfigContextView):
     base_template = 'virtualization/virtualmachine.html'
     base_template = 'virtualization/virtualmachine.html'
 
 
 
 
-class VirtualMachineEditView(ObjectEditView):
+class VirtualMachineEditView(generic.ObjectEditView):
     queryset = VirtualMachine.objects.all()
     queryset = VirtualMachine.objects.all()
     model_form = forms.VirtualMachineForm
     model_form = forms.VirtualMachineForm
     template_name = 'virtualization/virtualmachine_edit.html'
     template_name = 'virtualization/virtualmachine_edit.html'
 
 
 
 
-class VirtualMachineDeleteView(ObjectDeleteView):
+class VirtualMachineDeleteView(generic.ObjectDeleteView):
     queryset = VirtualMachine.objects.all()
     queryset = VirtualMachine.objects.all()
 
 
 
 
-class VirtualMachineBulkImportView(BulkImportView):
+class VirtualMachineBulkImportView(generic.BulkImportView):
     queryset = VirtualMachine.objects.all()
     queryset = VirtualMachine.objects.all()
     model_form = forms.VirtualMachineCSVForm
     model_form = forms.VirtualMachineCSVForm
     table = tables.VirtualMachineTable
     table = tables.VirtualMachineTable
 
 
 
 
-class VirtualMachineBulkEditView(BulkEditView):
+class VirtualMachineBulkEditView(generic.BulkEditView):
     queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role')
     queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role')
     filterset = filters.VirtualMachineFilterSet
     filterset = filters.VirtualMachineFilterSet
     table = tables.VirtualMachineTable
     table = tables.VirtualMachineTable
     form = forms.VirtualMachineBulkEditForm
     form = forms.VirtualMachineBulkEditForm
 
 
 
 
-class VirtualMachineBulkDeleteView(BulkDeleteView):
+class VirtualMachineBulkDeleteView(generic.BulkDeleteView):
     queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role')
     queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role')
     filterset = filters.VirtualMachineFilterSet
     filterset = filters.VirtualMachineFilterSet
     table = tables.VirtualMachineTable
     table = tables.VirtualMachineTable
@@ -310,7 +307,7 @@ class VirtualMachineBulkDeleteView(BulkDeleteView):
 # VM interfaces
 # VM interfaces
 #
 #
 
 
-class VMInterfaceListView(ObjectListView):
+class VMInterfaceListView(generic.ObjectListView):
     queryset = VMInterface.objects.all()
     queryset = VMInterface.objects.all()
     filterset = filters.VMInterfaceFilterSet
     filterset = filters.VMInterfaceFilterSet
     filterset_form = forms.VMInterfaceFilterForm
     filterset_form = forms.VMInterfaceFilterForm
@@ -318,7 +315,7 @@ class VMInterfaceListView(ObjectListView):
     action_buttons = ('export',)
     action_buttons = ('export',)
 
 
 
 
-class VMInterfaceView(ObjectView):
+class VMInterfaceView(generic.ObjectView):
     queryset = VMInterface.objects.all()
     queryset = VMInterface.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -353,41 +350,41 @@ class VMInterfaceView(ObjectView):
 
 
 
 
 # TODO: This should not use ComponentCreateView
 # TODO: This should not use ComponentCreateView
-class VMInterfaceCreateView(ComponentCreateView):
+class VMInterfaceCreateView(generic.ComponentCreateView):
     queryset = VMInterface.objects.all()
     queryset = VMInterface.objects.all()
     form = forms.VMInterfaceCreateForm
     form = forms.VMInterfaceCreateForm
     model_form = forms.VMInterfaceForm
     model_form = forms.VMInterfaceForm
     template_name = 'virtualization/virtualmachine_component_add.html'
     template_name = 'virtualization/virtualmachine_component_add.html'
 
 
 
 
-class VMInterfaceEditView(ObjectEditView):
+class VMInterfaceEditView(generic.ObjectEditView):
     queryset = VMInterface.objects.all()
     queryset = VMInterface.objects.all()
     model_form = forms.VMInterfaceForm
     model_form = forms.VMInterfaceForm
     template_name = 'virtualization/vminterface_edit.html'
     template_name = 'virtualization/vminterface_edit.html'
 
 
 
 
-class VMInterfaceDeleteView(ObjectDeleteView):
+class VMInterfaceDeleteView(generic.ObjectDeleteView):
     queryset = VMInterface.objects.all()
     queryset = VMInterface.objects.all()
 
 
 
 
-class VMInterfaceBulkImportView(BulkImportView):
+class VMInterfaceBulkImportView(generic.BulkImportView):
     queryset = VMInterface.objects.all()
     queryset = VMInterface.objects.all()
     model_form = forms.VMInterfaceCSVForm
     model_form = forms.VMInterfaceCSVForm
     table = tables.VMInterfaceTable
     table = tables.VMInterfaceTable
 
 
 
 
-class VMInterfaceBulkEditView(BulkEditView):
+class VMInterfaceBulkEditView(generic.BulkEditView):
     queryset = VMInterface.objects.all()
     queryset = VMInterface.objects.all()
     table = tables.VMInterfaceTable
     table = tables.VMInterfaceTable
     form = forms.VMInterfaceBulkEditForm
     form = forms.VMInterfaceBulkEditForm
 
 
 
 
-class VMInterfaceBulkRenameView(BulkRenameView):
+class VMInterfaceBulkRenameView(generic.BulkRenameView):
     queryset = VMInterface.objects.all()
     queryset = VMInterface.objects.all()
     form = forms.VMInterfaceBulkRenameForm
     form = forms.VMInterfaceBulkRenameForm
 
 
 
 
-class VMInterfaceBulkDeleteView(BulkDeleteView):
+class VMInterfaceBulkDeleteView(generic.BulkDeleteView):
     queryset = VMInterface.objects.all()
     queryset = VMInterface.objects.all()
     table = tables.VMInterfaceTable
     table = tables.VMInterfaceTable
 
 
@@ -396,7 +393,7 @@ class VMInterfaceBulkDeleteView(BulkDeleteView):
 # Bulk Device component creation
 # Bulk Device component creation
 #
 #
 
 
-class VirtualMachineBulkAddInterfaceView(BulkComponentCreateView):
+class VirtualMachineBulkAddInterfaceView(generic.BulkComponentCreateView):
     parent_model = VirtualMachine
     parent_model = VirtualMachine
     parent_field = 'virtual_machine'
     parent_field = 'virtual_machine'
     form = forms.VMInterfaceBulkCreateForm
     form = forms.VMInterfaceBulkCreateForm

Некоторые файлы не были показаны из-за большого количества измененных файлов