diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 0abe35b8d..2edb5a78e 100644 --- a/netbox/circuits/views.py +++ b/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_tables2 import RequestConfig +from netbox.views import generic from utilities.forms import ConfirmationForm 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 .choices import CircuitTerminationSideChoices from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -18,14 +16,14 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider # Providers # -class ProviderListView(ObjectListView): +class ProviderListView(generic.ObjectListView): queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering) filterset = filters.ProviderFilterSet filterset_form = forms.ProviderFilterForm table = tables.ProviderTable -class ProviderView(ObjectView): +class ProviderView(generic.ObjectView): queryset = Provider.objects.all() def get(self, request, slug): @@ -52,30 +50,30 @@ class ProviderView(ObjectView): }) -class ProviderEditView(ObjectEditView): +class ProviderEditView(generic.ObjectEditView): queryset = Provider.objects.all() model_form = forms.ProviderForm template_name = 'circuits/provider_edit.html' -class ProviderDeleteView(ObjectDeleteView): +class ProviderDeleteView(generic.ObjectDeleteView): queryset = Provider.objects.all() -class ProviderBulkImportView(BulkImportView): +class ProviderBulkImportView(generic.BulkImportView): queryset = Provider.objects.all() model_form = forms.ProviderCSVForm table = tables.ProviderTable -class ProviderBulkEditView(BulkEditView): +class ProviderBulkEditView(generic.BulkEditView): queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering) filterset = filters.ProviderFilterSet table = tables.ProviderTable form = forms.ProviderBulkEditForm -class ProviderBulkDeleteView(BulkDeleteView): +class ProviderBulkDeleteView(generic.BulkDeleteView): queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering) filterset = filters.ProviderFilterSet table = tables.ProviderTable @@ -85,27 +83,27 @@ class ProviderBulkDeleteView(BulkDeleteView): # Circuit Types # -class CircuitTypeListView(ObjectListView): +class CircuitTypeListView(generic.ObjectListView): queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')).order_by(*CircuitType._meta.ordering) table = tables.CircuitTypeTable -class CircuitTypeEditView(ObjectEditView): +class CircuitTypeEditView(generic.ObjectEditView): queryset = CircuitType.objects.all() model_form = forms.CircuitTypeForm -class CircuitTypeDeleteView(ObjectDeleteView): +class CircuitTypeDeleteView(generic.ObjectDeleteView): queryset = CircuitType.objects.all() -class CircuitTypeBulkImportView(BulkImportView): +class CircuitTypeBulkImportView(generic.BulkImportView): queryset = CircuitType.objects.all() model_form = forms.CircuitTypeCSVForm table = tables.CircuitTypeTable -class CircuitTypeBulkDeleteView(BulkDeleteView): +class CircuitTypeBulkDeleteView(generic.BulkDeleteView): queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')).order_by(*CircuitType._meta.ordering) table = tables.CircuitTypeTable @@ -114,7 +112,7 @@ class CircuitTypeBulkDeleteView(BulkDeleteView): # Circuits # -class CircuitListView(ObjectListView): +class CircuitListView(generic.ObjectListView): queryset = Circuit.objects.prefetch_related( 'provider', 'type', 'tenant', 'terminations' ).annotate_sites() @@ -123,7 +121,7 @@ class CircuitListView(ObjectListView): table = tables.CircuitTable -class CircuitView(ObjectView): +class CircuitView(generic.ObjectView): queryset = Circuit.objects.all() def get(self, request, pk): @@ -152,23 +150,23 @@ class CircuitView(ObjectView): }) -class CircuitEditView(ObjectEditView): +class CircuitEditView(generic.ObjectEditView): queryset = Circuit.objects.all() model_form = forms.CircuitForm template_name = 'circuits/circuit_edit.html' -class CircuitDeleteView(ObjectDeleteView): +class CircuitDeleteView(generic.ObjectDeleteView): queryset = Circuit.objects.all() -class CircuitBulkImportView(BulkImportView): +class CircuitBulkImportView(generic.BulkImportView): queryset = Circuit.objects.all() model_form = forms.CircuitCSVForm table = tables.CircuitTable -class CircuitBulkEditView(BulkEditView): +class CircuitBulkEditView(generic.BulkEditView): queryset = Circuit.objects.prefetch_related( 'provider', 'type', 'tenant', 'terminations' ) @@ -177,7 +175,7 @@ class CircuitBulkEditView(BulkEditView): form = forms.CircuitBulkEditForm -class CircuitBulkDeleteView(BulkDeleteView): +class CircuitBulkDeleteView(generic.BulkDeleteView): queryset = Circuit.objects.prefetch_related( 'provider', 'type', 'tenant', 'terminations' ) @@ -185,7 +183,7 @@ class CircuitBulkDeleteView(BulkDeleteView): table = tables.CircuitTable -class CircuitSwapTerminations(ObjectEditView): +class CircuitSwapTerminations(generic.ObjectEditView): """ Swap the A and Z terminations of a circuit. """ @@ -258,7 +256,7 @@ class CircuitSwapTerminations(ObjectEditView): # Circuit terminations # -class CircuitTerminationEditView(ObjectEditView): +class CircuitTerminationEditView(generic.ObjectEditView): queryset = CircuitTermination.objects.all() model_form = forms.CircuitTerminationForm template_name = 'circuits/circuittermination_edit.html' @@ -272,5 +270,5 @@ class CircuitTerminationEditView(ObjectEditView): return obj.circuit.get_absolute_url() -class CircuitTerminationDeleteView(ObjectDeleteView): +class CircuitTerminationDeleteView(generic.ObjectDeleteView): queryset = CircuitTermination.objects.all() diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 72b691528..7b0c26be7 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -7,7 +7,6 @@ from django.db import transaction from django.db.models import Count, F, Prefetch from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, modelformset_factory from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse from django.utils.html import escape from django.utils.safestring import mark_safe from django.views.generic import View @@ -16,16 +15,13 @@ from circuits.models import Circuit from extras.views import ObjectConfigContextView from ipam.models import IPAddress, Prefix, Service, VLAN from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable +from netbox.views import generic from secrets.models import Secret from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.permissions import get_permission_for_model from utilities.utils import csv_format, get_subquery -from utilities.views import ( - BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, BulkRenameView, ComponentCreateView, - GetReturnURLMixin, ObjectView, ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView, - ObjectPermissionRequiredMixin, -) +from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin from virtualization.models import VirtualMachine from . import filters, forms, tables from .choices import DeviceFaceChoices @@ -102,7 +98,7 @@ class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View) # Regions # -class RegionListView(ObjectListView): +class RegionListView(generic.ObjectListView): queryset = Region.objects.add_related_count( Region.objects.all(), Site, @@ -115,22 +111,22 @@ class RegionListView(ObjectListView): table = tables.RegionTable -class RegionEditView(ObjectEditView): +class RegionEditView(generic.ObjectEditView): queryset = Region.objects.all() model_form = forms.RegionForm -class RegionDeleteView(ObjectDeleteView): +class RegionDeleteView(generic.ObjectDeleteView): queryset = Region.objects.all() -class RegionBulkImportView(BulkImportView): +class RegionBulkImportView(generic.BulkImportView): queryset = Region.objects.all() model_form = forms.RegionCSVForm table = tables.RegionTable -class RegionBulkDeleteView(BulkDeleteView): +class RegionBulkDeleteView(generic.BulkDeleteView): queryset = Region.objects.add_related_count( Region.objects.all(), Site, @@ -146,14 +142,14 @@ class RegionBulkDeleteView(BulkDeleteView): # Sites # -class SiteListView(ObjectListView): +class SiteListView(generic.ObjectListView): queryset = Site.objects.all() filterset = filters.SiteFilterSet filterset_form = forms.SiteFilterForm table = tables.SiteTable -class SiteView(ObjectView): +class SiteView(generic.ObjectView): queryset = Site.objects.prefetch_related('region', 'tenant__group') def get(self, request, slug): @@ -182,30 +178,30 @@ class SiteView(ObjectView): }) -class SiteEditView(ObjectEditView): +class SiteEditView(generic.ObjectEditView): queryset = Site.objects.all() model_form = forms.SiteForm template_name = 'dcim/site_edit.html' -class SiteDeleteView(ObjectDeleteView): +class SiteDeleteView(generic.ObjectDeleteView): queryset = Site.objects.all() -class SiteBulkImportView(BulkImportView): +class SiteBulkImportView(generic.BulkImportView): queryset = Site.objects.all() model_form = forms.SiteCSVForm table = tables.SiteTable -class SiteBulkEditView(BulkEditView): +class SiteBulkEditView(generic.BulkEditView): queryset = Site.objects.prefetch_related('region', 'tenant') filterset = filters.SiteFilterSet table = tables.SiteTable form = forms.SiteBulkEditForm -class SiteBulkDeleteView(BulkDeleteView): +class SiteBulkDeleteView(generic.BulkDeleteView): queryset = Site.objects.prefetch_related('region', 'tenant') filterset = filters.SiteFilterSet table = tables.SiteTable @@ -215,7 +211,7 @@ class SiteBulkDeleteView(BulkDeleteView): # Rack groups # -class RackGroupListView(ObjectListView): +class RackGroupListView(generic.ObjectListView): queryset = RackGroup.objects.add_related_count( RackGroup.objects.all(), Rack, @@ -228,22 +224,22 @@ class RackGroupListView(ObjectListView): table = tables.RackGroupTable -class RackGroupEditView(ObjectEditView): +class RackGroupEditView(generic.ObjectEditView): queryset = RackGroup.objects.all() model_form = forms.RackGroupForm -class RackGroupDeleteView(ObjectDeleteView): +class RackGroupDeleteView(generic.ObjectDeleteView): queryset = RackGroup.objects.all() -class RackGroupBulkImportView(BulkImportView): +class RackGroupBulkImportView(generic.BulkImportView): queryset = RackGroup.objects.all() model_form = forms.RackGroupCSVForm table = tables.RackGroupTable -class RackGroupBulkDeleteView(BulkDeleteView): +class RackGroupBulkDeleteView(generic.BulkDeleteView): queryset = RackGroup.objects.add_related_count( RackGroup.objects.all(), Rack, @@ -259,27 +255,27 @@ class RackGroupBulkDeleteView(BulkDeleteView): # Rack roles # -class RackRoleListView(ObjectListView): +class RackRoleListView(generic.ObjectListView): queryset = RackRole.objects.annotate(rack_count=Count('racks')).order_by(*RackRole._meta.ordering) table = tables.RackRoleTable -class RackRoleEditView(ObjectEditView): +class RackRoleEditView(generic.ObjectEditView): queryset = RackRole.objects.all() model_form = forms.RackRoleForm -class RackRoleDeleteView(ObjectDeleteView): +class RackRoleDeleteView(generic.ObjectDeleteView): queryset = RackRole.objects.all() -class RackRoleBulkImportView(BulkImportView): +class RackRoleBulkImportView(generic.BulkImportView): queryset = RackRole.objects.all() model_form = forms.RackRoleCSVForm table = tables.RackRoleTable -class RackRoleBulkDeleteView(BulkDeleteView): +class RackRoleBulkDeleteView(generic.BulkDeleteView): queryset = RackRole.objects.annotate(rack_count=Count('racks')).order_by(*RackRole._meta.ordering) table = tables.RackRoleTable @@ -288,7 +284,7 @@ class RackRoleBulkDeleteView(BulkDeleteView): # Racks # -class RackListView(ObjectListView): +class RackListView(generic.ObjectListView): queryset = Rack.objects.annotate( device_count=Count('devices') ).order_by(*Rack._meta.ordering) @@ -297,7 +293,7 @@ class RackListView(ObjectListView): table = tables.RackDetailTable -class RackElevationListView(ObjectListView): +class RackElevationListView(generic.ObjectListView): """ Display a set of rack elevations side-by-side. """ @@ -339,7 +335,7 @@ class RackElevationListView(ObjectListView): }) -class RackView(ObjectView): +class RackView(generic.ObjectView): queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role') def get(self, request, pk): @@ -374,30 +370,30 @@ class RackView(ObjectView): }) -class RackEditView(ObjectEditView): +class RackEditView(generic.ObjectEditView): queryset = Rack.objects.all() model_form = forms.RackForm template_name = 'dcim/rack_edit.html' -class RackDeleteView(ObjectDeleteView): +class RackDeleteView(generic.ObjectDeleteView): queryset = Rack.objects.all() -class RackBulkImportView(BulkImportView): +class RackBulkImportView(generic.BulkImportView): queryset = Rack.objects.all() model_form = forms.RackCSVForm table = tables.RackTable -class RackBulkEditView(BulkEditView): +class RackBulkEditView(generic.BulkEditView): queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role') filterset = filters.RackFilterSet table = tables.RackTable form = forms.RackBulkEditForm -class RackBulkDeleteView(BulkDeleteView): +class RackBulkDeleteView(generic.BulkDeleteView): queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role') filterset = filters.RackFilterSet table = tables.RackTable @@ -407,14 +403,14 @@ class RackBulkDeleteView(BulkDeleteView): # Rack reservations # -class RackReservationListView(ObjectListView): +class RackReservationListView(generic.ObjectListView): queryset = RackReservation.objects.all() filterset = filters.RackReservationFilterSet filterset_form = forms.RackReservationFilterForm table = tables.RackReservationTable -class RackReservationView(ObjectView): +class RackReservationView(generic.ObjectView): queryset = RackReservation.objects.prefetch_related('rack') def get(self, request, pk): @@ -426,7 +422,7 @@ class RackReservationView(ObjectView): }) -class RackReservationEditView(ObjectEditView): +class RackReservationEditView(generic.ObjectEditView): queryset = RackReservation.objects.all() model_form = forms.RackReservationForm template_name = 'dcim/rackreservation_edit.html' @@ -439,11 +435,11 @@ class RackReservationEditView(ObjectEditView): return obj -class RackReservationDeleteView(ObjectDeleteView): +class RackReservationDeleteView(generic.ObjectDeleteView): queryset = RackReservation.objects.all() -class RackReservationImportView(BulkImportView): +class RackReservationImportView(generic.BulkImportView): queryset = RackReservation.objects.all() model_form = forms.RackReservationCSVForm table = tables.RackReservationTable @@ -459,14 +455,14 @@ class RackReservationImportView(BulkImportView): return instance -class RackReservationBulkEditView(BulkEditView): +class RackReservationBulkEditView(generic.BulkEditView): queryset = RackReservation.objects.prefetch_related('rack', 'user') filterset = filters.RackReservationFilterSet table = tables.RackReservationTable form = forms.RackReservationBulkEditForm -class RackReservationBulkDeleteView(BulkDeleteView): +class RackReservationBulkDeleteView(generic.BulkDeleteView): queryset = RackReservation.objects.prefetch_related('rack', 'user') filterset = filters.RackReservationFilterSet table = tables.RackReservationTable @@ -476,7 +472,7 @@ class RackReservationBulkDeleteView(BulkDeleteView): # Manufacturers # -class ManufacturerListView(ObjectListView): +class ManufacturerListView(generic.ObjectListView): queryset = Manufacturer.objects.annotate( devicetype_count=get_subquery(DeviceType, 'manufacturer'), inventoryitem_count=get_subquery(InventoryItem, 'manufacturer'), @@ -485,22 +481,22 @@ class ManufacturerListView(ObjectListView): table = tables.ManufacturerTable -class ManufacturerEditView(ObjectEditView): +class ManufacturerEditView(generic.ObjectEditView): queryset = Manufacturer.objects.all() model_form = forms.ManufacturerForm -class ManufacturerDeleteView(ObjectDeleteView): +class ManufacturerDeleteView(generic.ObjectDeleteView): queryset = Manufacturer.objects.all() -class ManufacturerBulkImportView(BulkImportView): +class ManufacturerBulkImportView(generic.BulkImportView): queryset = Manufacturer.objects.all() model_form = forms.ManufacturerCSVForm table = tables.ManufacturerTable -class ManufacturerBulkDeleteView(BulkDeleteView): +class ManufacturerBulkDeleteView(generic.BulkDeleteView): queryset = Manufacturer.objects.annotate( devicetype_count=Count('device_types') ).order_by(*Manufacturer._meta.ordering) @@ -511,7 +507,7 @@ class ManufacturerBulkDeleteView(BulkDeleteView): # Device types # -class DeviceTypeListView(ObjectListView): +class DeviceTypeListView(generic.ObjectListView): queryset = DeviceType.objects.annotate( instance_count=Count('instances') ).order_by(*DeviceType._meta.ordering) @@ -520,7 +516,7 @@ class DeviceTypeListView(ObjectListView): table = tables.DeviceTypeTable -class DeviceTypeView(ObjectView): +class DeviceTypeView(generic.ObjectView): queryset = DeviceType.objects.prefetch_related('manufacturer') def get(self, request, pk): @@ -585,17 +581,17 @@ class DeviceTypeView(ObjectView): }) -class DeviceTypeEditView(ObjectEditView): +class DeviceTypeEditView(generic.ObjectEditView): queryset = DeviceType.objects.all() model_form = forms.DeviceTypeForm template_name = 'dcim/devicetype_edit.html' -class DeviceTypeDeleteView(ObjectDeleteView): +class DeviceTypeDeleteView(generic.ObjectDeleteView): queryset = DeviceType.objects.all() -class DeviceTypeImportView(ObjectImportView): +class DeviceTypeImportView(generic.ObjectImportView): additional_permissions = [ 'dcim.add_devicetype', 'dcim.add_consoleporttemplate', @@ -621,7 +617,7 @@ class DeviceTypeImportView(ObjectImportView): )) -class DeviceTypeBulkEditView(BulkEditView): +class DeviceTypeBulkEditView(generic.BulkEditView): queryset = DeviceType.objects.prefetch_related('manufacturer').annotate( instance_count=Count('instances') ).order_by(*DeviceType._meta.ordering) @@ -630,7 +626,7 @@ class DeviceTypeBulkEditView(BulkEditView): form = forms.DeviceTypeBulkEditForm -class DeviceTypeBulkDeleteView(BulkDeleteView): +class DeviceTypeBulkDeleteView(generic.BulkDeleteView): queryset = DeviceType.objects.prefetch_related('manufacturer').annotate( instance_count=Count('instances') ).order_by(*DeviceType._meta.ordering) @@ -642,33 +638,33 @@ class DeviceTypeBulkDeleteView(BulkDeleteView): # Console port templates # -class ConsolePortTemplateCreateView(ComponentCreateView): +class ConsolePortTemplateCreateView(generic.ComponentCreateView): queryset = ConsolePortTemplate.objects.all() form = forms.ConsolePortTemplateCreateForm model_form = forms.ConsolePortTemplateForm template_name = 'dcim/device_component_add.html' -class ConsolePortTemplateEditView(ObjectEditView): +class ConsolePortTemplateEditView(generic.ObjectEditView): queryset = ConsolePortTemplate.objects.all() model_form = forms.ConsolePortTemplateForm -class ConsolePortTemplateDeleteView(ObjectDeleteView): +class ConsolePortTemplateDeleteView(generic.ObjectDeleteView): queryset = ConsolePortTemplate.objects.all() -class ConsolePortTemplateBulkEditView(BulkEditView): +class ConsolePortTemplateBulkEditView(generic.BulkEditView): queryset = ConsolePortTemplate.objects.all() table = tables.ConsolePortTemplateTable form = forms.ConsolePortTemplateBulkEditForm -class ConsolePortTemplateBulkRenameView(BulkRenameView): +class ConsolePortTemplateBulkRenameView(generic.BulkRenameView): queryset = ConsolePortTemplate.objects.all() -class ConsolePortTemplateBulkDeleteView(BulkDeleteView): +class ConsolePortTemplateBulkDeleteView(generic.BulkDeleteView): queryset = ConsolePortTemplate.objects.all() table = tables.ConsolePortTemplateTable @@ -677,33 +673,33 @@ class ConsolePortTemplateBulkDeleteView(BulkDeleteView): # Console server port templates # -class ConsoleServerPortTemplateCreateView(ComponentCreateView): +class ConsoleServerPortTemplateCreateView(generic.ComponentCreateView): queryset = ConsoleServerPortTemplate.objects.all() form = forms.ConsoleServerPortTemplateCreateForm model_form = forms.ConsoleServerPortTemplateForm template_name = 'dcim/device_component_add.html' -class ConsoleServerPortTemplateEditView(ObjectEditView): +class ConsoleServerPortTemplateEditView(generic.ObjectEditView): queryset = ConsoleServerPortTemplate.objects.all() model_form = forms.ConsoleServerPortTemplateForm -class ConsoleServerPortTemplateDeleteView(ObjectDeleteView): +class ConsoleServerPortTemplateDeleteView(generic.ObjectDeleteView): queryset = ConsoleServerPortTemplate.objects.all() -class ConsoleServerPortTemplateBulkEditView(BulkEditView): +class ConsoleServerPortTemplateBulkEditView(generic.BulkEditView): queryset = ConsoleServerPortTemplate.objects.all() table = tables.ConsoleServerPortTemplateTable form = forms.ConsoleServerPortTemplateBulkEditForm -class ConsoleServerPortTemplateBulkRenameView(BulkRenameView): +class ConsoleServerPortTemplateBulkRenameView(generic.BulkRenameView): queryset = ConsoleServerPortTemplate.objects.all() -class ConsoleServerPortTemplateBulkDeleteView(BulkDeleteView): +class ConsoleServerPortTemplateBulkDeleteView(generic.BulkDeleteView): queryset = ConsoleServerPortTemplate.objects.all() table = tables.ConsoleServerPortTemplateTable @@ -712,33 +708,33 @@ class ConsoleServerPortTemplateBulkDeleteView(BulkDeleteView): # Power port templates # -class PowerPortTemplateCreateView(ComponentCreateView): +class PowerPortTemplateCreateView(generic.ComponentCreateView): queryset = PowerPortTemplate.objects.all() form = forms.PowerPortTemplateCreateForm model_form = forms.PowerPortTemplateForm template_name = 'dcim/device_component_add.html' -class PowerPortTemplateEditView(ObjectEditView): +class PowerPortTemplateEditView(generic.ObjectEditView): queryset = PowerPortTemplate.objects.all() model_form = forms.PowerPortTemplateForm -class PowerPortTemplateDeleteView(ObjectDeleteView): +class PowerPortTemplateDeleteView(generic.ObjectDeleteView): queryset = PowerPortTemplate.objects.all() -class PowerPortTemplateBulkEditView(BulkEditView): +class PowerPortTemplateBulkEditView(generic.BulkEditView): queryset = PowerPortTemplate.objects.all() table = tables.PowerPortTemplateTable form = forms.PowerPortTemplateBulkEditForm -class PowerPortTemplateBulkRenameView(BulkRenameView): +class PowerPortTemplateBulkRenameView(generic.BulkRenameView): queryset = PowerPortTemplate.objects.all() -class PowerPortTemplateBulkDeleteView(BulkDeleteView): +class PowerPortTemplateBulkDeleteView(generic.BulkDeleteView): queryset = PowerPortTemplate.objects.all() table = tables.PowerPortTemplateTable @@ -747,33 +743,33 @@ class PowerPortTemplateBulkDeleteView(BulkDeleteView): # Power outlet templates # -class PowerOutletTemplateCreateView(ComponentCreateView): +class PowerOutletTemplateCreateView(generic.ComponentCreateView): queryset = PowerOutletTemplate.objects.all() form = forms.PowerOutletTemplateCreateForm model_form = forms.PowerOutletTemplateForm template_name = 'dcim/device_component_add.html' -class PowerOutletTemplateEditView(ObjectEditView): +class PowerOutletTemplateEditView(generic.ObjectEditView): queryset = PowerOutletTemplate.objects.all() model_form = forms.PowerOutletTemplateForm -class PowerOutletTemplateDeleteView(ObjectDeleteView): +class PowerOutletTemplateDeleteView(generic.ObjectDeleteView): queryset = PowerOutletTemplate.objects.all() -class PowerOutletTemplateBulkEditView(BulkEditView): +class PowerOutletTemplateBulkEditView(generic.BulkEditView): queryset = PowerOutletTemplate.objects.all() table = tables.PowerOutletTemplateTable form = forms.PowerOutletTemplateBulkEditForm -class PowerOutletTemplateBulkRenameView(BulkRenameView): +class PowerOutletTemplateBulkRenameView(generic.BulkRenameView): queryset = PowerOutletTemplate.objects.all() -class PowerOutletTemplateBulkDeleteView(BulkDeleteView): +class PowerOutletTemplateBulkDeleteView(generic.BulkDeleteView): queryset = PowerOutletTemplate.objects.all() table = tables.PowerOutletTemplateTable @@ -782,33 +778,33 @@ class PowerOutletTemplateBulkDeleteView(BulkDeleteView): # Interface templates # -class InterfaceTemplateCreateView(ComponentCreateView): +class InterfaceTemplateCreateView(generic.ComponentCreateView): queryset = InterfaceTemplate.objects.all() form = forms.InterfaceTemplateCreateForm model_form = forms.InterfaceTemplateForm template_name = 'dcim/device_component_add.html' -class InterfaceTemplateEditView(ObjectEditView): +class InterfaceTemplateEditView(generic.ObjectEditView): queryset = InterfaceTemplate.objects.all() model_form = forms.InterfaceTemplateForm -class InterfaceTemplateDeleteView(ObjectDeleteView): +class InterfaceTemplateDeleteView(generic.ObjectDeleteView): queryset = InterfaceTemplate.objects.all() -class InterfaceTemplateBulkEditView(BulkEditView): +class InterfaceTemplateBulkEditView(generic.BulkEditView): queryset = InterfaceTemplate.objects.all() table = tables.InterfaceTemplateTable form = forms.InterfaceTemplateBulkEditForm -class InterfaceTemplateBulkRenameView(BulkRenameView): +class InterfaceTemplateBulkRenameView(generic.BulkRenameView): queryset = InterfaceTemplate.objects.all() -class InterfaceTemplateBulkDeleteView(BulkDeleteView): +class InterfaceTemplateBulkDeleteView(generic.BulkDeleteView): queryset = InterfaceTemplate.objects.all() table = tables.InterfaceTemplateTable @@ -817,33 +813,33 @@ class InterfaceTemplateBulkDeleteView(BulkDeleteView): # Front port templates # -class FrontPortTemplateCreateView(ComponentCreateView): +class FrontPortTemplateCreateView(generic.ComponentCreateView): queryset = FrontPortTemplate.objects.all() form = forms.FrontPortTemplateCreateForm model_form = forms.FrontPortTemplateForm template_name = 'dcim/device_component_add.html' -class FrontPortTemplateEditView(ObjectEditView): +class FrontPortTemplateEditView(generic.ObjectEditView): queryset = FrontPortTemplate.objects.all() model_form = forms.FrontPortTemplateForm -class FrontPortTemplateDeleteView(ObjectDeleteView): +class FrontPortTemplateDeleteView(generic.ObjectDeleteView): queryset = FrontPortTemplate.objects.all() -class FrontPortTemplateBulkEditView(BulkEditView): +class FrontPortTemplateBulkEditView(generic.BulkEditView): queryset = FrontPortTemplate.objects.all() table = tables.FrontPortTemplateTable form = forms.FrontPortTemplateBulkEditForm -class FrontPortTemplateBulkRenameView(BulkRenameView): +class FrontPortTemplateBulkRenameView(generic.BulkRenameView): queryset = FrontPortTemplate.objects.all() -class FrontPortTemplateBulkDeleteView(BulkDeleteView): +class FrontPortTemplateBulkDeleteView(generic.BulkDeleteView): queryset = FrontPortTemplate.objects.all() table = tables.FrontPortTemplateTable @@ -852,33 +848,33 @@ class FrontPortTemplateBulkDeleteView(BulkDeleteView): # Rear port templates # -class RearPortTemplateCreateView(ComponentCreateView): +class RearPortTemplateCreateView(generic.ComponentCreateView): queryset = RearPortTemplate.objects.all() form = forms.RearPortTemplateCreateForm model_form = forms.RearPortTemplateForm template_name = 'dcim/device_component_add.html' -class RearPortTemplateEditView(ObjectEditView): +class RearPortTemplateEditView(generic.ObjectEditView): queryset = RearPortTemplate.objects.all() model_form = forms.RearPortTemplateForm -class RearPortTemplateDeleteView(ObjectDeleteView): +class RearPortTemplateDeleteView(generic.ObjectDeleteView): queryset = RearPortTemplate.objects.all() -class RearPortTemplateBulkEditView(BulkEditView): +class RearPortTemplateBulkEditView(generic.BulkEditView): queryset = RearPortTemplate.objects.all() table = tables.RearPortTemplateTable form = forms.RearPortTemplateBulkEditForm -class RearPortTemplateBulkRenameView(BulkRenameView): +class RearPortTemplateBulkRenameView(generic.BulkRenameView): queryset = RearPortTemplate.objects.all() -class RearPortTemplateBulkDeleteView(BulkDeleteView): +class RearPortTemplateBulkDeleteView(generic.BulkDeleteView): queryset = RearPortTemplate.objects.all() table = tables.RearPortTemplateTable @@ -887,33 +883,33 @@ class RearPortTemplateBulkDeleteView(BulkDeleteView): # Device bay templates # -class DeviceBayTemplateCreateView(ComponentCreateView): +class DeviceBayTemplateCreateView(generic.ComponentCreateView): queryset = DeviceBayTemplate.objects.all() form = forms.DeviceBayTemplateCreateForm model_form = forms.DeviceBayTemplateForm template_name = 'dcim/device_component_add.html' -class DeviceBayTemplateEditView(ObjectEditView): +class DeviceBayTemplateEditView(generic.ObjectEditView): queryset = DeviceBayTemplate.objects.all() model_form = forms.DeviceBayTemplateForm -class DeviceBayTemplateDeleteView(ObjectDeleteView): +class DeviceBayTemplateDeleteView(generic.ObjectDeleteView): queryset = DeviceBayTemplate.objects.all() -class DeviceBayTemplateBulkEditView(BulkEditView): +class DeviceBayTemplateBulkEditView(generic.BulkEditView): queryset = DeviceBayTemplate.objects.all() table = tables.DeviceBayTemplateTable form = forms.DeviceBayTemplateBulkEditForm -class DeviceBayTemplateBulkRenameView(BulkRenameView): +class DeviceBayTemplateBulkRenameView(generic.BulkRenameView): queryset = DeviceBayTemplate.objects.all() -class DeviceBayTemplateBulkDeleteView(BulkDeleteView): +class DeviceBayTemplateBulkDeleteView(generic.BulkDeleteView): queryset = DeviceBayTemplate.objects.all() table = tables.DeviceBayTemplateTable @@ -922,7 +918,7 @@ class DeviceBayTemplateBulkDeleteView(BulkDeleteView): # Device roles # -class DeviceRoleListView(ObjectListView): +class DeviceRoleListView(generic.ObjectListView): queryset = DeviceRole.objects.annotate( device_count=get_subquery(Device, 'device_role'), vm_count=get_subquery(VirtualMachine, 'role') @@ -930,22 +926,22 @@ class DeviceRoleListView(ObjectListView): table = tables.DeviceRoleTable -class DeviceRoleEditView(ObjectEditView): +class DeviceRoleEditView(generic.ObjectEditView): queryset = DeviceRole.objects.all() model_form = forms.DeviceRoleForm -class DeviceRoleDeleteView(ObjectDeleteView): +class DeviceRoleDeleteView(generic.ObjectDeleteView): queryset = DeviceRole.objects.all() -class DeviceRoleBulkImportView(BulkImportView): +class DeviceRoleBulkImportView(generic.BulkImportView): queryset = DeviceRole.objects.all() model_form = forms.DeviceRoleCSVForm table = tables.DeviceRoleTable -class DeviceRoleBulkDeleteView(BulkDeleteView): +class DeviceRoleBulkDeleteView(generic.BulkDeleteView): queryset = DeviceRole.objects.all() table = tables.DeviceRoleTable @@ -954,7 +950,7 @@ class DeviceRoleBulkDeleteView(BulkDeleteView): # Platforms # -class PlatformListView(ObjectListView): +class PlatformListView(generic.ObjectListView): queryset = Platform.objects.annotate( device_count=get_subquery(Device, 'platform'), vm_count=get_subquery(VirtualMachine, 'platform') @@ -962,22 +958,22 @@ class PlatformListView(ObjectListView): table = tables.PlatformTable -class PlatformEditView(ObjectEditView): +class PlatformEditView(generic.ObjectEditView): queryset = Platform.objects.all() model_form = forms.PlatformForm -class PlatformDeleteView(ObjectDeleteView): +class PlatformDeleteView(generic.ObjectDeleteView): queryset = Platform.objects.all() -class PlatformBulkImportView(BulkImportView): +class PlatformBulkImportView(generic.BulkImportView): queryset = Platform.objects.all() model_form = forms.PlatformCSVForm table = tables.PlatformTable -class PlatformBulkDeleteView(BulkDeleteView): +class PlatformBulkDeleteView(generic.BulkDeleteView): queryset = Platform.objects.all() table = tables.PlatformTable @@ -986,7 +982,7 @@ class PlatformBulkDeleteView(BulkDeleteView): # Devices # -class DeviceListView(ObjectListView): +class DeviceListView(generic.ObjectListView): queryset = Device.objects.all() filterset = filters.DeviceFilterSet filterset_form = forms.DeviceFilterForm @@ -994,7 +990,7 @@ class DeviceListView(ObjectListView): template_name = 'dcim/device_list.html' -class DeviceView(ObjectView): +class DeviceView(generic.ObjectView): queryset = Device.objects.prefetch_related( 'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform', 'primary_ip4', 'primary_ip6' ) @@ -1155,7 +1151,7 @@ class DeviceView(ObjectView): }) -class DeviceStatusView(ObjectView): +class DeviceStatusView(generic.ObjectView): additional_permissions = ['dcim.napalm_read_device'] queryset = Device.objects.all() @@ -1169,7 +1165,7 @@ class DeviceStatusView(ObjectView): }) -class DeviceLLDPNeighborsView(ObjectView): +class DeviceLLDPNeighborsView(generic.ObjectView): additional_permissions = ['dcim.napalm_read_device'] queryset = Device.objects.all() @@ -1187,7 +1183,7 @@ class DeviceLLDPNeighborsView(ObjectView): }) -class DeviceConfigView(ObjectView): +class DeviceConfigView(generic.ObjectView): additional_permissions = ['dcim.napalm_read_device'] queryset = Device.objects.all() @@ -1206,24 +1202,24 @@ class DeviceConfigContextView(ObjectConfigContextView): base_template = 'dcim/device.html' -class DeviceEditView(ObjectEditView): +class DeviceEditView(generic.ObjectEditView): queryset = Device.objects.all() model_form = forms.DeviceForm template_name = 'dcim/device_edit.html' -class DeviceDeleteView(ObjectDeleteView): +class DeviceDeleteView(generic.ObjectDeleteView): queryset = Device.objects.all() -class DeviceBulkImportView(BulkImportView): +class DeviceBulkImportView(generic.BulkImportView): queryset = Device.objects.all() model_form = forms.DeviceCSVForm table = tables.DeviceImportTable template_name = 'dcim/device_import.html' -class ChildDeviceBulkImportView(BulkImportView): +class ChildDeviceBulkImportView(generic.BulkImportView): queryset = Device.objects.all() model_form = forms.ChildDeviceCSVForm table = tables.DeviceImportTable @@ -1241,14 +1237,14 @@ class ChildDeviceBulkImportView(BulkImportView): return obj -class DeviceBulkEditView(BulkEditView): +class DeviceBulkEditView(generic.BulkEditView): queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer') filterset = filters.DeviceFilterSet table = tables.DeviceTable form = forms.DeviceBulkEditForm -class DeviceBulkDeleteView(BulkDeleteView): +class DeviceBulkDeleteView(generic.BulkDeleteView): queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer') filterset = filters.DeviceFilterSet table = tables.DeviceTable @@ -1258,7 +1254,7 @@ class DeviceBulkDeleteView(BulkDeleteView): # Console ports # -class ConsolePortListView(ObjectListView): +class ConsolePortListView(generic.ObjectListView): queryset = ConsolePort.objects.all() filterset = filters.ConsolePortFilterSet filterset_form = forms.ConsolePortFilterForm @@ -1266,41 +1262,41 @@ class ConsolePortListView(ObjectListView): action_buttons = ('import', 'export') -class ConsolePortView(ObjectView): +class ConsolePortView(generic.ObjectView): queryset = ConsolePort.objects.all() -class ConsolePortCreateView(ComponentCreateView): +class ConsolePortCreateView(generic.ComponentCreateView): queryset = ConsolePort.objects.all() form = forms.ConsolePortCreateForm model_form = forms.ConsolePortForm template_name = 'dcim/device_component_add.html' -class ConsolePortEditView(ObjectEditView): +class ConsolePortEditView(generic.ObjectEditView): queryset = ConsolePort.objects.all() model_form = forms.ConsolePortForm template_name = 'dcim/device_component_edit.html' -class ConsolePortDeleteView(ObjectDeleteView): +class ConsolePortDeleteView(generic.ObjectDeleteView): queryset = ConsolePort.objects.all() -class ConsolePortBulkImportView(BulkImportView): +class ConsolePortBulkImportView(generic.BulkImportView): queryset = ConsolePort.objects.all() model_form = forms.ConsolePortCSVForm table = tables.ConsolePortTable -class ConsolePortBulkEditView(BulkEditView): +class ConsolePortBulkEditView(generic.BulkEditView): queryset = ConsolePort.objects.all() filterset = filters.ConsolePortFilterSet table = tables.ConsolePortTable form = forms.ConsolePortBulkEditForm -class ConsolePortBulkRenameView(BulkRenameView): +class ConsolePortBulkRenameView(generic.BulkRenameView): queryset = ConsolePort.objects.all() @@ -1308,7 +1304,7 @@ class ConsolePortBulkDisconnectView(BulkDisconnectView): queryset = ConsolePort.objects.all() -class ConsolePortBulkDeleteView(BulkDeleteView): +class ConsolePortBulkDeleteView(generic.BulkDeleteView): queryset = ConsolePort.objects.all() filterset = filters.ConsolePortFilterSet table = tables.ConsolePortTable @@ -1318,7 +1314,7 @@ class ConsolePortBulkDeleteView(BulkDeleteView): # Console server ports # -class ConsoleServerPortListView(ObjectListView): +class ConsoleServerPortListView(generic.ObjectListView): queryset = ConsoleServerPort.objects.all() filterset = filters.ConsoleServerPortFilterSet filterset_form = forms.ConsoleServerPortFilterForm @@ -1326,41 +1322,41 @@ class ConsoleServerPortListView(ObjectListView): action_buttons = ('import', 'export') -class ConsoleServerPortView(ObjectView): +class ConsoleServerPortView(generic.ObjectView): queryset = ConsoleServerPort.objects.all() -class ConsoleServerPortCreateView(ComponentCreateView): +class ConsoleServerPortCreateView(generic.ComponentCreateView): queryset = ConsoleServerPort.objects.all() form = forms.ConsoleServerPortCreateForm model_form = forms.ConsoleServerPortForm template_name = 'dcim/device_component_add.html' -class ConsoleServerPortEditView(ObjectEditView): +class ConsoleServerPortEditView(generic.ObjectEditView): queryset = ConsoleServerPort.objects.all() model_form = forms.ConsoleServerPortForm template_name = 'dcim/device_component_edit.html' -class ConsoleServerPortDeleteView(ObjectDeleteView): +class ConsoleServerPortDeleteView(generic.ObjectDeleteView): queryset = ConsoleServerPort.objects.all() -class ConsoleServerPortBulkImportView(BulkImportView): +class ConsoleServerPortBulkImportView(generic.BulkImportView): queryset = ConsoleServerPort.objects.all() model_form = forms.ConsoleServerPortCSVForm table = tables.ConsoleServerPortTable -class ConsoleServerPortBulkEditView(BulkEditView): +class ConsoleServerPortBulkEditView(generic.BulkEditView): queryset = ConsoleServerPort.objects.all() filterset = filters.ConsoleServerPortFilterSet table = tables.ConsoleServerPortTable form = forms.ConsoleServerPortBulkEditForm -class ConsoleServerPortBulkRenameView(BulkRenameView): +class ConsoleServerPortBulkRenameView(generic.BulkRenameView): queryset = ConsoleServerPort.objects.all() @@ -1368,7 +1364,7 @@ class ConsoleServerPortBulkDisconnectView(BulkDisconnectView): queryset = ConsoleServerPort.objects.all() -class ConsoleServerPortBulkDeleteView(BulkDeleteView): +class ConsoleServerPortBulkDeleteView(generic.BulkDeleteView): queryset = ConsoleServerPort.objects.all() filterset = filters.ConsoleServerPortFilterSet table = tables.ConsoleServerPortTable @@ -1378,7 +1374,7 @@ class ConsoleServerPortBulkDeleteView(BulkDeleteView): # Power ports # -class PowerPortListView(ObjectListView): +class PowerPortListView(generic.ObjectListView): queryset = PowerPort.objects.all() filterset = filters.PowerPortFilterSet filterset_form = forms.PowerPortFilterForm @@ -1386,41 +1382,41 @@ class PowerPortListView(ObjectListView): action_buttons = ('import', 'export') -class PowerPortView(ObjectView): +class PowerPortView(generic.ObjectView): queryset = PowerPort.objects.all() -class PowerPortCreateView(ComponentCreateView): +class PowerPortCreateView(generic.ComponentCreateView): queryset = PowerPort.objects.all() form = forms.PowerPortCreateForm model_form = forms.PowerPortForm template_name = 'dcim/device_component_add.html' -class PowerPortEditView(ObjectEditView): +class PowerPortEditView(generic.ObjectEditView): queryset = PowerPort.objects.all() model_form = forms.PowerPortForm template_name = 'dcim/device_component_edit.html' -class PowerPortDeleteView(ObjectDeleteView): +class PowerPortDeleteView(generic.ObjectDeleteView): queryset = PowerPort.objects.all() -class PowerPortBulkImportView(BulkImportView): +class PowerPortBulkImportView(generic.BulkImportView): queryset = PowerPort.objects.all() model_form = forms.PowerPortCSVForm table = tables.PowerPortTable -class PowerPortBulkEditView(BulkEditView): +class PowerPortBulkEditView(generic.BulkEditView): queryset = PowerPort.objects.all() filterset = filters.PowerPortFilterSet table = tables.PowerPortTable form = forms.PowerPortBulkEditForm -class PowerPortBulkRenameView(BulkRenameView): +class PowerPortBulkRenameView(generic.BulkRenameView): queryset = PowerPort.objects.all() @@ -1428,7 +1424,7 @@ class PowerPortBulkDisconnectView(BulkDisconnectView): queryset = PowerPort.objects.all() -class PowerPortBulkDeleteView(BulkDeleteView): +class PowerPortBulkDeleteView(generic.BulkDeleteView): queryset = PowerPort.objects.all() filterset = filters.PowerPortFilterSet table = tables.PowerPortTable @@ -1438,7 +1434,7 @@ class PowerPortBulkDeleteView(BulkDeleteView): # Power outlets # -class PowerOutletListView(ObjectListView): +class PowerOutletListView(generic.ObjectListView): queryset = PowerOutlet.objects.all() filterset = filters.PowerOutletFilterSet filterset_form = forms.PowerOutletFilterForm @@ -1446,41 +1442,41 @@ class PowerOutletListView(ObjectListView): action_buttons = ('import', 'export') -class PowerOutletView(ObjectView): +class PowerOutletView(generic.ObjectView): queryset = PowerOutlet.objects.all() -class PowerOutletCreateView(ComponentCreateView): +class PowerOutletCreateView(generic.ComponentCreateView): queryset = PowerOutlet.objects.all() form = forms.PowerOutletCreateForm model_form = forms.PowerOutletForm template_name = 'dcim/device_component_add.html' -class PowerOutletEditView(ObjectEditView): +class PowerOutletEditView(generic.ObjectEditView): queryset = PowerOutlet.objects.all() model_form = forms.PowerOutletForm template_name = 'dcim/device_component_edit.html' -class PowerOutletDeleteView(ObjectDeleteView): +class PowerOutletDeleteView(generic.ObjectDeleteView): queryset = PowerOutlet.objects.all() -class PowerOutletBulkImportView(BulkImportView): +class PowerOutletBulkImportView(generic.BulkImportView): queryset = PowerOutlet.objects.all() model_form = forms.PowerOutletCSVForm table = tables.PowerOutletTable -class PowerOutletBulkEditView(BulkEditView): +class PowerOutletBulkEditView(generic.BulkEditView): queryset = PowerOutlet.objects.all() filterset = filters.PowerOutletFilterSet table = tables.PowerOutletTable form = forms.PowerOutletBulkEditForm -class PowerOutletBulkRenameView(BulkRenameView): +class PowerOutletBulkRenameView(generic.BulkRenameView): queryset = PowerOutlet.objects.all() @@ -1488,7 +1484,7 @@ class PowerOutletBulkDisconnectView(BulkDisconnectView): queryset = PowerOutlet.objects.all() -class PowerOutletBulkDeleteView(BulkDeleteView): +class PowerOutletBulkDeleteView(generic.BulkDeleteView): queryset = PowerOutlet.objects.all() filterset = filters.PowerOutletFilterSet table = tables.PowerOutletTable @@ -1498,7 +1494,7 @@ class PowerOutletBulkDeleteView(BulkDeleteView): # Interfaces # -class InterfaceListView(ObjectListView): +class InterfaceListView(generic.ObjectListView): queryset = Interface.objects.all() filterset = filters.InterfaceFilterSet filterset_form = forms.InterfaceFilterForm @@ -1506,7 +1502,7 @@ class InterfaceListView(ObjectListView): action_buttons = ('import', 'export') -class InterfaceView(ObjectView): +class InterfaceView(generic.ObjectView): queryset = Interface.objects.all() def get(self, request, pk): @@ -1540,37 +1536,37 @@ class InterfaceView(ObjectView): }) -class InterfaceCreateView(ComponentCreateView): +class InterfaceCreateView(generic.ComponentCreateView): queryset = Interface.objects.all() form = forms.InterfaceCreateForm model_form = forms.InterfaceForm template_name = 'dcim/device_component_add.html' -class InterfaceEditView(ObjectEditView): +class InterfaceEditView(generic.ObjectEditView): queryset = Interface.objects.all() model_form = forms.InterfaceForm template_name = 'dcim/interface_edit.html' -class InterfaceDeleteView(ObjectDeleteView): +class InterfaceDeleteView(generic.ObjectDeleteView): queryset = Interface.objects.all() -class InterfaceBulkImportView(BulkImportView): +class InterfaceBulkImportView(generic.BulkImportView): queryset = Interface.objects.all() model_form = forms.InterfaceCSVForm table = tables.InterfaceTable -class InterfaceBulkEditView(BulkEditView): +class InterfaceBulkEditView(generic.BulkEditView): queryset = Interface.objects.all() filterset = filters.InterfaceFilterSet table = tables.InterfaceTable form = forms.InterfaceBulkEditForm -class InterfaceBulkRenameView(BulkRenameView): +class InterfaceBulkRenameView(generic.BulkRenameView): queryset = Interface.objects.all() @@ -1578,7 +1574,7 @@ class InterfaceBulkDisconnectView(BulkDisconnectView): queryset = Interface.objects.all() -class InterfaceBulkDeleteView(BulkDeleteView): +class InterfaceBulkDeleteView(generic.BulkDeleteView): queryset = Interface.objects.all() filterset = filters.InterfaceFilterSet table = tables.InterfaceTable @@ -1588,7 +1584,7 @@ class InterfaceBulkDeleteView(BulkDeleteView): # Front ports # -class FrontPortListView(ObjectListView): +class FrontPortListView(generic.ObjectListView): queryset = FrontPort.objects.all() filterset = filters.FrontPortFilterSet filterset_form = forms.FrontPortFilterForm @@ -1596,41 +1592,41 @@ class FrontPortListView(ObjectListView): action_buttons = ('import', 'export') -class FrontPortView(ObjectView): +class FrontPortView(generic.ObjectView): queryset = FrontPort.objects.all() -class FrontPortCreateView(ComponentCreateView): +class FrontPortCreateView(generic.ComponentCreateView): queryset = FrontPort.objects.all() form = forms.FrontPortCreateForm model_form = forms.FrontPortForm template_name = 'dcim/device_component_add.html' -class FrontPortEditView(ObjectEditView): +class FrontPortEditView(generic.ObjectEditView): queryset = FrontPort.objects.all() model_form = forms.FrontPortForm template_name = 'dcim/device_component_edit.html' -class FrontPortDeleteView(ObjectDeleteView): +class FrontPortDeleteView(generic.ObjectDeleteView): queryset = FrontPort.objects.all() -class FrontPortBulkImportView(BulkImportView): +class FrontPortBulkImportView(generic.BulkImportView): queryset = FrontPort.objects.all() model_form = forms.FrontPortCSVForm table = tables.FrontPortTable -class FrontPortBulkEditView(BulkEditView): +class FrontPortBulkEditView(generic.BulkEditView): queryset = FrontPort.objects.all() filterset = filters.FrontPortFilterSet table = tables.FrontPortTable form = forms.FrontPortBulkEditForm -class FrontPortBulkRenameView(BulkRenameView): +class FrontPortBulkRenameView(generic.BulkRenameView): queryset = FrontPort.objects.all() @@ -1638,7 +1634,7 @@ class FrontPortBulkDisconnectView(BulkDisconnectView): queryset = FrontPort.objects.all() -class FrontPortBulkDeleteView(BulkDeleteView): +class FrontPortBulkDeleteView(generic.BulkDeleteView): queryset = FrontPort.objects.all() filterset = filters.FrontPortFilterSet table = tables.FrontPortTable @@ -1648,7 +1644,7 @@ class FrontPortBulkDeleteView(BulkDeleteView): # Rear ports # -class RearPortListView(ObjectListView): +class RearPortListView(generic.ObjectListView): queryset = RearPort.objects.all() filterset = filters.RearPortFilterSet filterset_form = forms.RearPortFilterForm @@ -1656,41 +1652,41 @@ class RearPortListView(ObjectListView): action_buttons = ('import', 'export') -class RearPortView(ObjectView): +class RearPortView(generic.ObjectView): queryset = RearPort.objects.all() -class RearPortCreateView(ComponentCreateView): +class RearPortCreateView(generic.ComponentCreateView): queryset = RearPort.objects.all() form = forms.RearPortCreateForm model_form = forms.RearPortForm template_name = 'dcim/device_component_add.html' -class RearPortEditView(ObjectEditView): +class RearPortEditView(generic.ObjectEditView): queryset = RearPort.objects.all() model_form = forms.RearPortForm template_name = 'dcim/device_component_edit.html' -class RearPortDeleteView(ObjectDeleteView): +class RearPortDeleteView(generic.ObjectDeleteView): queryset = RearPort.objects.all() -class RearPortBulkImportView(BulkImportView): +class RearPortBulkImportView(generic.BulkImportView): queryset = RearPort.objects.all() model_form = forms.RearPortCSVForm table = tables.RearPortTable -class RearPortBulkEditView(BulkEditView): +class RearPortBulkEditView(generic.BulkEditView): queryset = RearPort.objects.all() filterset = filters.RearPortFilterSet table = tables.RearPortTable form = forms.RearPortBulkEditForm -class RearPortBulkRenameView(BulkRenameView): +class RearPortBulkRenameView(generic.BulkRenameView): queryset = RearPort.objects.all() @@ -1698,7 +1694,7 @@ class RearPortBulkDisconnectView(BulkDisconnectView): queryset = RearPort.objects.all() -class RearPortBulkDeleteView(BulkDeleteView): +class RearPortBulkDeleteView(generic.BulkDeleteView): queryset = RearPort.objects.all() filterset = filters.RearPortFilterSet table = tables.RearPortTable @@ -1708,7 +1704,7 @@ class RearPortBulkDeleteView(BulkDeleteView): # Device bays # -class DeviceBayListView(ObjectListView): +class DeviceBayListView(generic.ObjectListView): queryset = DeviceBay.objects.all() filterset = filters.DeviceBayFilterSet filterset_form = forms.DeviceBayFilterForm @@ -1716,28 +1712,28 @@ class DeviceBayListView(ObjectListView): action_buttons = ('import', 'export') -class DeviceBayView(ObjectView): +class DeviceBayView(generic.ObjectView): queryset = DeviceBay.objects.all() -class DeviceBayCreateView(ComponentCreateView): +class DeviceBayCreateView(generic.ComponentCreateView): queryset = DeviceBay.objects.all() form = forms.DeviceBayCreateForm model_form = forms.DeviceBayForm template_name = 'dcim/device_component_add.html' -class DeviceBayEditView(ObjectEditView): +class DeviceBayEditView(generic.ObjectEditView): queryset = DeviceBay.objects.all() model_form = forms.DeviceBayForm template_name = 'dcim/device_component_edit.html' -class DeviceBayDeleteView(ObjectDeleteView): +class DeviceBayDeleteView(generic.ObjectDeleteView): queryset = DeviceBay.objects.all() -class DeviceBayPopulateView(ObjectEditView): +class DeviceBayPopulateView(generic.ObjectEditView): queryset = DeviceBay.objects.all() def get(self, request, pk): @@ -1769,7 +1765,7 @@ class DeviceBayPopulateView(ObjectEditView): }) -class DeviceBayDepopulateView(ObjectEditView): +class DeviceBayDepopulateView(generic.ObjectEditView): queryset = DeviceBay.objects.all() def get(self, request, pk): @@ -1802,24 +1798,24 @@ class DeviceBayDepopulateView(ObjectEditView): }) -class DeviceBayBulkImportView(BulkImportView): +class DeviceBayBulkImportView(generic.BulkImportView): queryset = DeviceBay.objects.all() model_form = forms.DeviceBayCSVForm table = tables.DeviceBayTable -class DeviceBayBulkEditView(BulkEditView): +class DeviceBayBulkEditView(generic.BulkEditView): queryset = DeviceBay.objects.all() filterset = filters.DeviceBayFilterSet table = tables.DeviceBayTable form = forms.DeviceBayBulkEditForm -class DeviceBayBulkRenameView(BulkRenameView): +class DeviceBayBulkRenameView(generic.BulkRenameView): queryset = DeviceBay.objects.all() -class DeviceBayBulkDeleteView(BulkDeleteView): +class DeviceBayBulkDeleteView(generic.BulkDeleteView): queryset = DeviceBay.objects.all() filterset = filters.DeviceBayFilterSet table = tables.DeviceBayTable @@ -1829,7 +1825,7 @@ class DeviceBayBulkDeleteView(BulkDeleteView): # Inventory items # -class InventoryItemListView(ObjectListView): +class InventoryItemListView(generic.ObjectListView): queryset = InventoryItem.objects.all() filterset = filters.InventoryItemFilterSet filterset_form = forms.InventoryItemFilterForm @@ -1837,44 +1833,44 @@ class InventoryItemListView(ObjectListView): action_buttons = ('import', 'export') -class InventoryItemView(ObjectView): +class InventoryItemView(generic.ObjectView): queryset = InventoryItem.objects.all() -class InventoryItemEditView(ObjectEditView): +class InventoryItemEditView(generic.ObjectEditView): queryset = InventoryItem.objects.all() model_form = forms.InventoryItemForm -class InventoryItemCreateView(ComponentCreateView): +class InventoryItemCreateView(generic.ComponentCreateView): queryset = InventoryItem.objects.all() form = forms.InventoryItemCreateForm model_form = forms.InventoryItemForm template_name = 'dcim/device_component_add.html' -class InventoryItemDeleteView(ObjectDeleteView): +class InventoryItemDeleteView(generic.ObjectDeleteView): queryset = InventoryItem.objects.all() -class InventoryItemBulkImportView(BulkImportView): +class InventoryItemBulkImportView(generic.BulkImportView): queryset = InventoryItem.objects.all() model_form = forms.InventoryItemCSVForm table = tables.InventoryItemTable -class InventoryItemBulkEditView(BulkEditView): +class InventoryItemBulkEditView(generic.BulkEditView): queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer') filterset = filters.InventoryItemFilterSet table = tables.InventoryItemTable form = forms.InventoryItemBulkEditForm -class InventoryItemBulkRenameView(BulkRenameView): +class InventoryItemBulkRenameView(generic.BulkRenameView): queryset = InventoryItem.objects.all() -class InventoryItemBulkDeleteView(BulkDeleteView): +class InventoryItemBulkDeleteView(generic.BulkDeleteView): queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer') table = tables.InventoryItemTable template_name = 'dcim/inventoryitem_bulk_delete.html' @@ -1884,7 +1880,7 @@ class InventoryItemBulkDeleteView(BulkDeleteView): # Bulk Device component creation # -class DeviceBulkAddConsolePortView(BulkComponentCreateView): +class DeviceBulkAddConsolePortView(generic.BulkComponentCreateView): parent_model = Device parent_field = 'device' form = forms.ConsolePortBulkCreateForm @@ -1895,7 +1891,7 @@ class DeviceBulkAddConsolePortView(BulkComponentCreateView): default_return_url = 'dcim:device_list' -class DeviceBulkAddConsoleServerPortView(BulkComponentCreateView): +class DeviceBulkAddConsoleServerPortView(generic.BulkComponentCreateView): parent_model = Device parent_field = 'device' form = forms.ConsoleServerPortBulkCreateForm @@ -1906,7 +1902,7 @@ class DeviceBulkAddConsoleServerPortView(BulkComponentCreateView): default_return_url = 'dcim:device_list' -class DeviceBulkAddPowerPortView(BulkComponentCreateView): +class DeviceBulkAddPowerPortView(generic.BulkComponentCreateView): parent_model = Device parent_field = 'device' form = forms.PowerPortBulkCreateForm @@ -1917,7 +1913,7 @@ class DeviceBulkAddPowerPortView(BulkComponentCreateView): default_return_url = 'dcim:device_list' -class DeviceBulkAddPowerOutletView(BulkComponentCreateView): +class DeviceBulkAddPowerOutletView(generic.BulkComponentCreateView): parent_model = Device parent_field = 'device' form = forms.PowerOutletBulkCreateForm @@ -1928,7 +1924,7 @@ class DeviceBulkAddPowerOutletView(BulkComponentCreateView): default_return_url = 'dcim:device_list' -class DeviceBulkAddInterfaceView(BulkComponentCreateView): +class DeviceBulkAddInterfaceView(generic.BulkComponentCreateView): parent_model = Device parent_field = 'device' form = forms.InterfaceBulkCreateForm @@ -1939,7 +1935,7 @@ class DeviceBulkAddInterfaceView(BulkComponentCreateView): default_return_url = 'dcim:device_list' -# class DeviceBulkAddFrontPortView(BulkComponentCreateView): +# class DeviceBulkAddFrontPortView(generic.BulkComponentCreateView): # parent_model = Device # parent_field = 'device' # form = forms.FrontPortBulkCreateForm @@ -1950,7 +1946,7 @@ class DeviceBulkAddInterfaceView(BulkComponentCreateView): # default_return_url = 'dcim:device_list' -class DeviceBulkAddRearPortView(BulkComponentCreateView): +class DeviceBulkAddRearPortView(generic.BulkComponentCreateView): parent_model = Device parent_field = 'device' form = forms.RearPortBulkCreateForm @@ -1961,7 +1957,7 @@ class DeviceBulkAddRearPortView(BulkComponentCreateView): default_return_url = 'dcim:device_list' -class DeviceBulkAddDeviceBayView(BulkComponentCreateView): +class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView): parent_model = Device parent_field = 'device' form = forms.DeviceBayBulkCreateForm @@ -1972,7 +1968,7 @@ class DeviceBulkAddDeviceBayView(BulkComponentCreateView): default_return_url = 'dcim:device_list' -class DeviceBulkAddInventoryItemView(BulkComponentCreateView): +class DeviceBulkAddInventoryItemView(generic.BulkComponentCreateView): parent_model = Device parent_field = 'device' form = forms.InventoryItemBulkCreateForm @@ -1987,7 +1983,7 @@ class DeviceBulkAddInventoryItemView(BulkComponentCreateView): # Cables # -class CableListView(ObjectListView): +class CableListView(generic.ObjectListView): queryset = Cable.objects.all() filterset = filters.CableFilterSet filterset_form = forms.CableFilterForm @@ -1995,7 +1991,7 @@ class CableListView(ObjectListView): action_buttons = ('import', 'export') -class CableView(ObjectView): +class CableView(generic.ObjectView): queryset = Cable.objects.all() def get(self, request, pk): @@ -2007,7 +2003,7 @@ class CableView(ObjectView): }) -class PathTraceView(ObjectView): +class PathTraceView(generic.ObjectView): """ Trace a cable path beginning from the given path endpoint (origin). """ @@ -2048,7 +2044,7 @@ class PathTraceView(ObjectView): }) -class CableCreateView(ObjectEditView): +class CableCreateView(generic.ObjectEditView): queryset = Cable.objects.all() template_name = 'dcim/cable_connect.html' @@ -2107,30 +2103,30 @@ class CableCreateView(ObjectEditView): }) -class CableEditView(ObjectEditView): +class CableEditView(generic.ObjectEditView): queryset = Cable.objects.all() model_form = forms.CableForm template_name = 'dcim/cable_edit.html' -class CableDeleteView(ObjectDeleteView): +class CableDeleteView(generic.ObjectDeleteView): queryset = Cable.objects.all() -class CableBulkImportView(BulkImportView): +class CableBulkImportView(generic.BulkImportView): queryset = Cable.objects.all() model_form = forms.CableCSVForm table = tables.CableTable -class CableBulkEditView(BulkEditView): +class CableBulkEditView(generic.BulkEditView): queryset = Cable.objects.prefetch_related('termination_a', 'termination_b') filterset = filters.CableFilterSet table = tables.CableTable form = forms.CableBulkEditForm -class CableBulkDeleteView(BulkDeleteView): +class CableBulkDeleteView(generic.BulkDeleteView): queryset = Cable.objects.prefetch_related('termination_a', 'termination_b') filterset = filters.CableFilterSet table = tables.CableTable @@ -2140,7 +2136,7 @@ class CableBulkDeleteView(BulkDeleteView): # Connections # -class ConsoleConnectionsListView(ObjectListView): +class ConsoleConnectionsListView(generic.ObjectListView): queryset = ConsolePort.objects.filter(_path__isnull=False).order_by('device') filterset = filters.ConsoleConnectionFilterSet filterset_form = forms.ConsoleConnectionFilterForm @@ -2170,7 +2166,7 @@ class ConsoleConnectionsListView(ObjectListView): } -class PowerConnectionsListView(ObjectListView): +class PowerConnectionsListView(generic.ObjectListView): queryset = PowerPort.objects.filter(_path__isnull=False).order_by('device') filterset = filters.PowerConnectionFilterSet filterset_form = forms.PowerConnectionFilterForm @@ -2200,7 +2196,7 @@ class PowerConnectionsListView(ObjectListView): } -class InterfaceConnectionsListView(ObjectListView): +class InterfaceConnectionsListView(generic.ObjectListView): queryset = Interface.objects.filter( # Avoid duplicate connections by only selecting the lower PK in a connected pair _path__isnull=False, @@ -2240,7 +2236,7 @@ class InterfaceConnectionsListView(ObjectListView): # Virtual chassis # -class VirtualChassisListView(ObjectListView): +class VirtualChassisListView(generic.ObjectListView): queryset = VirtualChassis.objects.annotate( member_count=Count('members', distinct=True) ).order_by(*VirtualChassis._meta.ordering) @@ -2249,7 +2245,7 @@ class VirtualChassisListView(ObjectListView): filterset_form = forms.VirtualChassisFilterForm -class VirtualChassisView(ObjectView): +class VirtualChassisView(generic.ObjectView): queryset = VirtualChassis.objects.all() def get(self, request, pk): @@ -2262,7 +2258,7 @@ class VirtualChassisView(ObjectView): }) -class VirtualChassisCreateView(ObjectEditView): +class VirtualChassisCreateView(generic.ObjectEditView): queryset = VirtualChassis.objects.all() model_form = forms.VirtualChassisCreateForm template_name = 'dcim/virtualchassis_add.html' @@ -2336,7 +2332,7 @@ class VirtualChassisEditView(ObjectPermissionRequiredMixin, GetReturnURLMixin, V }) -class VirtualChassisDeleteView(ObjectDeleteView): +class VirtualChassisDeleteView(generic.ObjectDeleteView): queryset = VirtualChassis.objects.all() @@ -2447,20 +2443,20 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL }) -class VirtualChassisBulkImportView(BulkImportView): +class VirtualChassisBulkImportView(generic.BulkImportView): queryset = VirtualChassis.objects.all() model_form = forms.VirtualChassisCSVForm table = tables.VirtualChassisTable -class VirtualChassisBulkEditView(BulkEditView): +class VirtualChassisBulkEditView(generic.BulkEditView): queryset = VirtualChassis.objects.all() filterset = filters.VirtualChassisFilterSet table = tables.VirtualChassisTable form = forms.VirtualChassisBulkEditForm -class VirtualChassisBulkDeleteView(BulkDeleteView): +class VirtualChassisBulkDeleteView(generic.BulkDeleteView): queryset = VirtualChassis.objects.all() filterset = filters.VirtualChassisFilterSet table = tables.VirtualChassisTable @@ -2470,7 +2466,7 @@ class VirtualChassisBulkDeleteView(BulkDeleteView): # Power panels # -class PowerPanelListView(ObjectListView): +class PowerPanelListView(generic.ObjectListView): queryset = PowerPanel.objects.annotate( powerfeed_count=Count('powerfeeds') ).order_by(*PowerPanel._meta.ordering) @@ -2479,7 +2475,7 @@ class PowerPanelListView(ObjectListView): table = tables.PowerPanelTable -class PowerPanelView(ObjectView): +class PowerPanelView(generic.ObjectView): queryset = PowerPanel.objects.prefetch_related('site', 'rack_group') def get(self, request, pk): @@ -2498,30 +2494,30 @@ class PowerPanelView(ObjectView): }) -class PowerPanelEditView(ObjectEditView): +class PowerPanelEditView(generic.ObjectEditView): queryset = PowerPanel.objects.all() model_form = forms.PowerPanelForm template_name = 'dcim/powerpanel_edit.html' -class PowerPanelDeleteView(ObjectDeleteView): +class PowerPanelDeleteView(generic.ObjectDeleteView): queryset = PowerPanel.objects.all() -class PowerPanelBulkImportView(BulkImportView): +class PowerPanelBulkImportView(generic.BulkImportView): queryset = PowerPanel.objects.all() model_form = forms.PowerPanelCSVForm table = tables.PowerPanelTable -class PowerPanelBulkEditView(BulkEditView): +class PowerPanelBulkEditView(generic.BulkEditView): queryset = PowerPanel.objects.prefetch_related('site', 'rack_group') filterset = filters.PowerPanelFilterSet table = tables.PowerPanelTable form = forms.PowerPanelBulkEditForm -class PowerPanelBulkDeleteView(BulkDeleteView): +class PowerPanelBulkDeleteView(generic.BulkDeleteView): queryset = PowerPanel.objects.prefetch_related( 'site', 'rack_group' ).annotate( @@ -2535,14 +2531,14 @@ class PowerPanelBulkDeleteView(BulkDeleteView): # Power feeds # -class PowerFeedListView(ObjectListView): +class PowerFeedListView(generic.ObjectListView): queryset = PowerFeed.objects.all() filterset = filters.PowerFeedFilterSet filterset_form = forms.PowerFeedFilterForm table = tables.PowerFeedTable -class PowerFeedView(ObjectView): +class PowerFeedView(generic.ObjectView): queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack') def get(self, request, pk): @@ -2554,30 +2550,30 @@ class PowerFeedView(ObjectView): }) -class PowerFeedEditView(ObjectEditView): +class PowerFeedEditView(generic.ObjectEditView): queryset = PowerFeed.objects.all() model_form = forms.PowerFeedForm template_name = 'dcim/powerfeed_edit.html' -class PowerFeedDeleteView(ObjectDeleteView): +class PowerFeedDeleteView(generic.ObjectDeleteView): queryset = PowerFeed.objects.all() -class PowerFeedBulkImportView(BulkImportView): +class PowerFeedBulkImportView(generic.BulkImportView): queryset = PowerFeed.objects.all() model_form = forms.PowerFeedCSVForm table = tables.PowerFeedTable -class PowerFeedBulkEditView(BulkEditView): +class PowerFeedBulkEditView(generic.BulkEditView): queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack') filterset = filters.PowerFeedFilterSet table = tables.PowerFeedTable form = forms.PowerFeedBulkEditForm -class PowerFeedBulkDeleteView(BulkDeleteView): +class PowerFeedBulkDeleteView(generic.BulkDeleteView): queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack') filterset = filters.PowerFeedFilterSet table = tables.PowerFeedTable diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 8d09e0610..9fb6c6deb 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -10,14 +10,12 @@ from django_tables2 import RequestConfig from rq import Worker from dcim.models import DeviceRole, Platform, Region, Site +from netbox.views import generic from tenancy.models import Tenant, TenantGroup from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator, get_paginate_count 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 . import filters, forms, tables from .choices import JobResultStatusChoices @@ -30,7 +28,7 @@ from .scripts import get_scripts, run_script # Tags # -class TagListView(ObjectListView): +class TagListView(generic.ObjectListView): queryset = Tag.objects.annotate( items=Count('extras_taggeditem_items') ).order_by(*Tag._meta.ordering) @@ -39,23 +37,23 @@ class TagListView(ObjectListView): table = tables.TagTable -class TagEditView(ObjectEditView): +class TagEditView(generic.ObjectEditView): queryset = Tag.objects.all() model_form = forms.TagForm template_name = 'extras/tag_edit.html' -class TagDeleteView(ObjectDeleteView): +class TagDeleteView(generic.ObjectDeleteView): queryset = Tag.objects.all() -class TagBulkImportView(BulkImportView): +class TagBulkImportView(generic.BulkImportView): queryset = Tag.objects.all() model_form = forms.TagCSVForm table = tables.TagTable -class TagBulkEditView(BulkEditView): +class TagBulkEditView(generic.BulkEditView): queryset = Tag.objects.annotate( items=Count('extras_taggeditem_items') ).order_by(*Tag._meta.ordering) @@ -63,7 +61,7 @@ class TagBulkEditView(BulkEditView): form = forms.TagBulkEditForm -class TagBulkDeleteView(BulkDeleteView): +class TagBulkDeleteView(generic.BulkDeleteView): queryset = Tag.objects.annotate( items=Count('extras_taggeditem_items') ).order_by(*Tag._meta.ordering) @@ -74,7 +72,7 @@ class TagBulkDeleteView(BulkDeleteView): # Config contexts # -class ConfigContextListView(ObjectListView): +class ConfigContextListView(generic.ObjectListView): queryset = ConfigContext.objects.all() filterset = filters.ConfigContextFilterSet filterset_form = forms.ConfigContextFilterForm @@ -82,7 +80,7 @@ class ConfigContextListView(ObjectListView): action_buttons = ('add',) -class ConfigContextView(ObjectView): +class ConfigContextView(generic.ObjectView): queryset = ConfigContext.objects.all() def get(self, request, pk): @@ -116,29 +114,29 @@ class ConfigContextView(ObjectView): }) -class ConfigContextEditView(ObjectEditView): +class ConfigContextEditView(generic.ObjectEditView): queryset = ConfigContext.objects.all() model_form = forms.ConfigContextForm template_name = 'extras/configcontext_edit.html' -class ConfigContextBulkEditView(BulkEditView): +class ConfigContextBulkEditView(generic.BulkEditView): queryset = ConfigContext.objects.all() filterset = filters.ConfigContextFilterSet table = tables.ConfigContextTable form = forms.ConfigContextBulkEditForm -class ConfigContextDeleteView(ObjectDeleteView): +class ConfigContextDeleteView(generic.ObjectDeleteView): queryset = ConfigContext.objects.all() -class ConfigContextBulkDeleteView(BulkDeleteView): +class ConfigContextBulkDeleteView(generic.BulkDeleteView): queryset = ConfigContext.objects.all() table = tables.ConfigContextTable -class ObjectConfigContextView(ObjectView): +class ObjectConfigContextView(generic.ObjectView): base_template = None def get(self, request, pk): @@ -172,7 +170,7 @@ class ObjectConfigContextView(ObjectView): # Change logging # -class ObjectChangeListView(ObjectListView): +class ObjectChangeListView(generic.ObjectListView): queryset = ObjectChange.objects.all() filterset = filters.ObjectChangeFilterSet filterset_form = forms.ObjectChangeFilterForm @@ -181,7 +179,7 @@ class ObjectChangeListView(ObjectListView): action_buttons = ('export',) -class ObjectChangeView(ObjectView): +class ObjectChangeView(generic.ObjectView): queryset = ObjectChange.objects.all() def get(self, request, pk): @@ -283,7 +281,7 @@ class ObjectChangeLogView(View): # Image attachments # -class ImageAttachmentEditView(ObjectEditView): +class ImageAttachmentEditView(generic.ObjectEditView): queryset = ImageAttachment.objects.all() model_form = forms.ImageAttachmentForm @@ -298,7 +296,7 @@ class ImageAttachmentEditView(ObjectEditView): return imageattachment.parent.get_absolute_url() -class ImageAttachmentDeleteView(ObjectDeleteView): +class ImageAttachmentDeleteView(generic.ObjectDeleteView): queryset = ImageAttachment.objects.all() def get_return_url(self, request, imageattachment): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 8fef56edf..d2248e84c 100644 --- a/netbox/ipam/views.py +++ b/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.expressions import RawSQL from django.shortcuts import get_object_or_404, redirect, render from django_tables2 import RequestConfig from dcim.models import Device, Interface +from netbox.views import generic from utilities.paginator import EnhancedPaginator, get_paginate_count 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 . import filters, forms, tables -from .choices import * from .constants import * 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 @@ -24,14 +18,14 @@ from .utils import add_available_ipaddresses, add_available_prefixes, add_availa # VRFs # -class VRFListView(ObjectListView): +class VRFListView(generic.ObjectListView): queryset = VRF.objects.all() filterset = filters.VRFFilterSet filterset_form = forms.VRFFilterForm table = tables.VRFTable -class VRFView(ObjectView): +class VRFView(generic.ObjectView): queryset = VRF.objects.all() def get(self, request, pk): @@ -56,30 +50,30 @@ class VRFView(ObjectView): }) -class VRFEditView(ObjectEditView): +class VRFEditView(generic.ObjectEditView): queryset = VRF.objects.all() model_form = forms.VRFForm template_name = 'ipam/vrf_edit.html' -class VRFDeleteView(ObjectDeleteView): +class VRFDeleteView(generic.ObjectDeleteView): queryset = VRF.objects.all() -class VRFBulkImportView(BulkImportView): +class VRFBulkImportView(generic.BulkImportView): queryset = VRF.objects.all() model_form = forms.VRFCSVForm table = tables.VRFTable -class VRFBulkEditView(BulkEditView): +class VRFBulkEditView(generic.BulkEditView): queryset = VRF.objects.prefetch_related('tenant') filterset = filters.VRFFilterSet table = tables.VRFTable form = forms.VRFBulkEditForm -class VRFBulkDeleteView(BulkDeleteView): +class VRFBulkDeleteView(generic.BulkDeleteView): queryset = VRF.objects.prefetch_related('tenant') filterset = filters.VRFFilterSet table = tables.VRFTable @@ -89,14 +83,14 @@ class VRFBulkDeleteView(BulkDeleteView): # Route targets # -class RouteTargetListView(ObjectListView): +class RouteTargetListView(generic.ObjectListView): queryset = RouteTarget.objects.all() filterset = filters.RouteTargetFilterSet filterset_form = forms.RouteTargetFilterForm table = tables.RouteTargetTable -class RouteTargetView(ObjectView): +class RouteTargetView(generic.ObjectView): queryset = RouteTarget.objects.all() def get(self, request, pk): @@ -118,29 +112,29 @@ class RouteTargetView(ObjectView): }) -class RouteTargetEditView(ObjectEditView): +class RouteTargetEditView(generic.ObjectEditView): queryset = RouteTarget.objects.all() model_form = forms.RouteTargetForm -class RouteTargetDeleteView(ObjectDeleteView): +class RouteTargetDeleteView(generic.ObjectDeleteView): queryset = RouteTarget.objects.all() -class RouteTargetBulkImportView(BulkImportView): +class RouteTargetBulkImportView(generic.BulkImportView): queryset = RouteTarget.objects.all() model_form = forms.RouteTargetCSVForm table = tables.RouteTargetTable -class RouteTargetBulkEditView(BulkEditView): +class RouteTargetBulkEditView(generic.BulkEditView): queryset = RouteTarget.objects.prefetch_related('tenant') filterset = filters.RouteTargetFilterSet table = tables.RouteTargetTable form = forms.RouteTargetBulkEditForm -class RouteTargetBulkDeleteView(BulkDeleteView): +class RouteTargetBulkDeleteView(generic.BulkDeleteView): queryset = RouteTarget.objects.prefetch_related('tenant') filterset = filters.RouteTargetFilterSet table = tables.RouteTargetTable @@ -150,7 +144,7 @@ class RouteTargetBulkDeleteView(BulkDeleteView): # RIRs # -class RIRListView(ObjectListView): +class RIRListView(generic.ObjectListView): queryset = RIR.objects.annotate(aggregate_count=Count('aggregates')).order_by(*RIR._meta.ordering) filterset = filters.RIRFilterSet filterset_form = forms.RIRFilterForm @@ -158,22 +152,22 @@ class RIRListView(ObjectListView): template_name = 'ipam/rir_list.html' -class RIREditView(ObjectEditView): +class RIREditView(generic.ObjectEditView): queryset = RIR.objects.all() model_form = forms.RIRForm -class RIRDeleteView(ObjectDeleteView): +class RIRDeleteView(generic.ObjectDeleteView): queryset = RIR.objects.all() -class RIRBulkImportView(BulkImportView): +class RIRBulkImportView(generic.BulkImportView): queryset = RIR.objects.all() model_form = forms.RIRCSVForm table = tables.RIRTable -class RIRBulkDeleteView(BulkDeleteView): +class RIRBulkDeleteView(generic.BulkDeleteView): queryset = RIR.objects.annotate(aggregate_count=Count('aggregates')).order_by(*RIR._meta.ordering) filterset = filters.RIRFilterSet table = tables.RIRTable @@ -183,7 +177,7 @@ class RIRBulkDeleteView(BulkDeleteView): # Aggregates # -class AggregateListView(ObjectListView): +class AggregateListView(generic.ObjectListView): queryset = Aggregate.objects.annotate( child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ()) ).order_by(*Aggregate._meta.ordering) @@ -209,7 +203,7 @@ class AggregateListView(ObjectListView): } -class AggregateView(ObjectView): +class AggregateView(generic.ObjectView): queryset = Aggregate.objects.all() def get(self, request, pk): @@ -254,30 +248,30 @@ class AggregateView(ObjectView): }) -class AggregateEditView(ObjectEditView): +class AggregateEditView(generic.ObjectEditView): queryset = Aggregate.objects.all() model_form = forms.AggregateForm template_name = 'ipam/aggregate_edit.html' -class AggregateDeleteView(ObjectDeleteView): +class AggregateDeleteView(generic.ObjectDeleteView): queryset = Aggregate.objects.all() -class AggregateBulkImportView(BulkImportView): +class AggregateBulkImportView(generic.BulkImportView): queryset = Aggregate.objects.all() model_form = forms.AggregateCSVForm table = tables.AggregateTable -class AggregateBulkEditView(BulkEditView): +class AggregateBulkEditView(generic.BulkEditView): queryset = Aggregate.objects.prefetch_related('rir') filterset = filters.AggregateFilterSet table = tables.AggregateTable form = forms.AggregateBulkEditForm -class AggregateBulkDeleteView(BulkDeleteView): +class AggregateBulkDeleteView(generic.BulkDeleteView): queryset = Aggregate.objects.prefetch_related('rir') filterset = filters.AggregateFilterSet table = tables.AggregateTable @@ -287,7 +281,7 @@ class AggregateBulkDeleteView(BulkDeleteView): # Prefix/VLAN roles # -class RoleListView(ObjectListView): +class RoleListView(generic.ObjectListView): queryset = Role.objects.annotate( prefix_count=get_subquery(Prefix, 'role'), vlan_count=get_subquery(VLAN, 'role') @@ -295,22 +289,22 @@ class RoleListView(ObjectListView): table = tables.RoleTable -class RoleEditView(ObjectEditView): +class RoleEditView(generic.ObjectEditView): queryset = Role.objects.all() model_form = forms.RoleForm -class RoleDeleteView(ObjectDeleteView): +class RoleDeleteView(generic.ObjectDeleteView): queryset = Role.objects.all() -class RoleBulkImportView(BulkImportView): +class RoleBulkImportView(generic.BulkImportView): queryset = Role.objects.all() model_form = forms.RoleCSVForm table = tables.RoleTable -class RoleBulkDeleteView(BulkDeleteView): +class RoleBulkDeleteView(generic.BulkDeleteView): queryset = Role.objects.all() table = tables.RoleTable @@ -319,7 +313,7 @@ class RoleBulkDeleteView(BulkDeleteView): # Prefixes # -class PrefixListView(ObjectListView): +class PrefixListView(generic.ObjectListView): queryset = Prefix.objects.annotate_tree() filterset = filters.PrefixFilterSet filterset_form = forms.PrefixFilterForm @@ -327,7 +321,7 @@ class PrefixListView(ObjectListView): 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') def get(self, request, pk): @@ -371,7 +365,7 @@ class PrefixView(ObjectView): }) -class PrefixPrefixesView(ObjectView): +class PrefixPrefixesView(generic.ObjectView): queryset = Prefix.objects.all() def get(self, request, pk): @@ -415,7 +409,7 @@ class PrefixPrefixesView(ObjectView): }) -class PrefixIPAddressesView(ObjectView): +class PrefixIPAddressesView(generic.ObjectView): queryset = Prefix.objects.all() def get(self, request, pk): @@ -459,31 +453,31 @@ class PrefixIPAddressesView(ObjectView): }) -class PrefixEditView(ObjectEditView): +class PrefixEditView(generic.ObjectEditView): queryset = Prefix.objects.all() model_form = forms.PrefixForm template_name = 'ipam/prefix_edit.html' -class PrefixDeleteView(ObjectDeleteView): +class PrefixDeleteView(generic.ObjectDeleteView): queryset = Prefix.objects.all() template_name = 'ipam/prefix_delete.html' -class PrefixBulkImportView(BulkImportView): +class PrefixBulkImportView(generic.BulkImportView): queryset = Prefix.objects.all() model_form = forms.PrefixCSVForm table = tables.PrefixTable -class PrefixBulkEditView(BulkEditView): +class PrefixBulkEditView(generic.BulkEditView): queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') filterset = filters.PrefixFilterSet table = tables.PrefixTable form = forms.PrefixBulkEditForm -class PrefixBulkDeleteView(BulkDeleteView): +class PrefixBulkDeleteView(generic.BulkDeleteView): queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') filterset = filters.PrefixFilterSet table = tables.PrefixTable @@ -493,14 +487,14 @@ class PrefixBulkDeleteView(BulkDeleteView): # IP addresses # -class IPAddressListView(ObjectListView): +class IPAddressListView(generic.ObjectListView): queryset = IPAddress.objects.all() filterset = filters.IPAddressFilterSet filterset_form = forms.IPAddressFilterForm table = tables.IPAddressDetailTable -class IPAddressView(ObjectView): +class IPAddressView(generic.ObjectView): queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant') def get(self, request, pk): @@ -553,7 +547,7 @@ class IPAddressView(ObjectView): }) -class IPAddressEditView(ObjectEditView): +class IPAddressEditView(generic.ObjectEditView): queryset = IPAddress.objects.all() model_form = forms.IPAddressForm template_name = 'ipam/ipaddress_edit.html' @@ -575,7 +569,7 @@ class IPAddressEditView(ObjectEditView): return obj -class IPAddressAssignView(ObjectView): +class IPAddressAssignView(generic.ObjectView): """ 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() -class IPAddressBulkCreateView(BulkCreateView): +class IPAddressBulkCreateView(generic.BulkCreateView): queryset = IPAddress.objects.all() form = forms.IPAddressBulkCreateForm model_form = forms.IPAddressBulkAddForm @@ -627,20 +621,20 @@ class IPAddressBulkCreateView(BulkCreateView): template_name = 'ipam/ipaddress_bulk_add.html' -class IPAddressBulkImportView(BulkImportView): +class IPAddressBulkImportView(generic.BulkImportView): queryset = IPAddress.objects.all() model_form = forms.IPAddressCSVForm table = tables.IPAddressTable -class IPAddressBulkEditView(BulkEditView): +class IPAddressBulkEditView(generic.BulkEditView): queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant') filterset = filters.IPAddressFilterSet table = tables.IPAddressTable form = forms.IPAddressBulkEditForm -class IPAddressBulkDeleteView(BulkDeleteView): +class IPAddressBulkDeleteView(generic.BulkDeleteView): queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant') filterset = filters.IPAddressFilterSet table = tables.IPAddressTable @@ -650,7 +644,7 @@ class IPAddressBulkDeleteView(BulkDeleteView): # VLAN groups # -class VLANGroupListView(ObjectListView): +class VLANGroupListView(generic.ObjectListView): queryset = VLANGroup.objects.annotate( vlan_count=Count('vlans') ).order_by(*VLANGroup._meta.ordering) @@ -659,22 +653,22 @@ class VLANGroupListView(ObjectListView): table = tables.VLANGroupTable -class VLANGroupEditView(ObjectEditView): +class VLANGroupEditView(generic.ObjectEditView): queryset = VLANGroup.objects.all() model_form = forms.VLANGroupForm -class VLANGroupDeleteView(ObjectDeleteView): +class VLANGroupDeleteView(generic.ObjectDeleteView): queryset = VLANGroup.objects.all() -class VLANGroupBulkImportView(BulkImportView): +class VLANGroupBulkImportView(generic.BulkImportView): queryset = VLANGroup.objects.all() model_form = forms.VLANGroupCSVForm table = tables.VLANGroupTable -class VLANGroupBulkDeleteView(BulkDeleteView): +class VLANGroupBulkDeleteView(generic.BulkDeleteView): queryset = VLANGroup.objects.prefetch_related('site').annotate( vlan_count=Count('vlans') ).order_by(*VLANGroup._meta.ordering) @@ -682,7 +676,7 @@ class VLANGroupBulkDeleteView(BulkDeleteView): table = tables.VLANGroupTable -class VLANGroupVLANsView(ObjectView): +class VLANGroupVLANsView(generic.ObjectView): queryset = VLANGroup.objects.all() def get(self, request, pk): @@ -725,14 +719,14 @@ class VLANGroupVLANsView(ObjectView): # VLANs # -class VLANListView(ObjectListView): +class VLANListView(generic.ObjectListView): queryset = VLAN.objects.all() filterset = filters.VLANFilterSet filterset_form = forms.VLANFilterForm table = tables.VLANDetailTable -class VLANView(ObjectView): +class VLANView(generic.ObjectView): queryset = VLAN.objects.prefetch_related('site__region', 'tenant__group', 'role') def get(self, request, pk): @@ -750,7 +744,7 @@ class VLANView(ObjectView): }) -class VLANInterfacesView(ObjectView): +class VLANInterfacesView(generic.ObjectView): queryset = VLAN.objects.all() def get(self, request, pk): @@ -771,7 +765,7 @@ class VLANInterfacesView(ObjectView): }) -class VLANVMInterfacesView(ObjectView): +class VLANVMInterfacesView(generic.ObjectView): queryset = VLAN.objects.all() def get(self, request, pk): @@ -792,30 +786,30 @@ class VLANVMInterfacesView(ObjectView): }) -class VLANEditView(ObjectEditView): +class VLANEditView(generic.ObjectEditView): queryset = VLAN.objects.all() model_form = forms.VLANForm template_name = 'ipam/vlan_edit.html' -class VLANDeleteView(ObjectDeleteView): +class VLANDeleteView(generic.ObjectDeleteView): queryset = VLAN.objects.all() -class VLANBulkImportView(BulkImportView): +class VLANBulkImportView(generic.BulkImportView): queryset = VLAN.objects.all() model_form = forms.VLANCSVForm table = tables.VLANTable -class VLANBulkEditView(BulkEditView): +class VLANBulkEditView(generic.BulkEditView): queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role') filterset = filters.VLANFilterSet table = tables.VLANTable form = forms.VLANBulkEditForm -class VLANBulkDeleteView(BulkDeleteView): +class VLANBulkDeleteView(generic.BulkDeleteView): queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role') filterset = filters.VLANFilterSet table = tables.VLANTable @@ -825,7 +819,7 @@ class VLANBulkDeleteView(BulkDeleteView): # Services # -class ServiceListView(ObjectListView): +class ServiceListView(generic.ObjectListView): queryset = Service.objects.all() filterset = filters.ServiceFilterSet filterset_form = forms.ServiceFilterForm @@ -833,7 +827,7 @@ class ServiceListView(ObjectListView): action_buttons = ('export',) -class ServiceView(ObjectView): +class ServiceView(generic.ObjectView): queryset = Service.objects.prefetch_related('ipaddresses') 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') model_form = forms.ServiceForm template_name = 'ipam/service_edit.html' @@ -864,24 +858,24 @@ class ServiceEditView(ObjectEditView): return obj -class ServiceBulkImportView(BulkImportView): +class ServiceBulkImportView(generic.BulkImportView): queryset = Service.objects.all() model_form = forms.ServiceCSVForm table = tables.ServiceTable -class ServiceDeleteView(ObjectDeleteView): +class ServiceDeleteView(generic.ObjectDeleteView): queryset = Service.objects.all() -class ServiceBulkEditView(BulkEditView): +class ServiceBulkEditView(generic.BulkEditView): queryset = Service.objects.prefetch_related('device', 'virtual_machine') filterset = filters.ServiceFilterSet table = tables.ServiceTable form = forms.ServiceBulkEditForm -class ServiceBulkDeleteView(BulkDeleteView): +class ServiceBulkDeleteView(generic.BulkDeleteView): queryset = Service.objects.prefetch_related('device', 'virtual_machine') filterset = filters.ServiceFilterSet table = tables.ServiceTable diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index 566d46eee..1395cbd1f 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -8,8 +8,8 @@ from django.http import Http404, HttpResponseRedirect from django.urls import reverse 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.views import server_error class LoginRequiredMiddleware(object): diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index dc24cc9eb..8215afe1c 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -94,4 +94,4 @@ urlpatterns = [ path('{}'.format(settings.BASE_PATH), include(_patterns)) ] -handler500 = 'utilities.views.server_error' +handler500 = 'netbox.views.server_error' diff --git a/netbox/netbox/views.py b/netbox/netbox/views/__init__.py similarity index 84% rename from netbox/netbox/views.py rename to netbox/netbox/views/__init__.py index 1b3e02b5b..5406e7206 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views/__init__.py @@ -1,22 +1,32 @@ +import platform +import sys + from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.db.models import F +from django.http import HttpResponseServerError from django.shortcuts import render +from django.template import loader +from django.template.exceptions import TemplateDoesNotExist 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 packaging import version 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.models import ObjectChange, JobResult 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 secrets.models import Secret from tenancy.models import Tenant from virtualization.models import Cluster, VirtualMachine -from .constants import SEARCH_MAX_RESULTS, SEARCH_TYPES -from .forms import SearchForm class HomeView(View): @@ -157,3 +167,22 @@ class StaticMediaFailureView(View): return render(request, 'media_failure.html', { '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('

Server Error (500)

', 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(), + })) diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py new file mode 100644 index 000000000..782d03737 --- /dev/null +++ b/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 = '{} {}'.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: {}'.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), + }) diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 4442341d7..539cbb160 100644 --- a/netbox/secrets/views.py +++ b/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.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 .models import SecretRole, Secret, SessionKey, UserKey @@ -28,27 +26,27 @@ def get_session_key(request): # Secret roles # -class SecretRoleListView(ObjectListView): +class SecretRoleListView(generic.ObjectListView): queryset = SecretRole.objects.annotate(secret_count=Count('secrets')).order_by(*SecretRole._meta.ordering) table = tables.SecretRoleTable -class SecretRoleEditView(ObjectEditView): +class SecretRoleEditView(generic.ObjectEditView): queryset = SecretRole.objects.all() model_form = forms.SecretRoleForm -class SecretRoleDeleteView(ObjectDeleteView): +class SecretRoleDeleteView(generic.ObjectDeleteView): queryset = SecretRole.objects.all() -class SecretRoleBulkImportView(BulkImportView): +class SecretRoleBulkImportView(generic.BulkImportView): queryset = SecretRole.objects.all() model_form = forms.SecretRoleCSVForm table = tables.SecretRoleTable -class SecretRoleBulkDeleteView(BulkDeleteView): +class SecretRoleBulkDeleteView(generic.BulkDeleteView): queryset = SecretRole.objects.annotate(secret_count=Count('secrets')).order_by(*SecretRole._meta.ordering) table = tables.SecretRoleTable @@ -57,7 +55,7 @@ class SecretRoleBulkDeleteView(BulkDeleteView): # Secrets # -class SecretListView(ObjectListView): +class SecretListView(generic.ObjectListView): queryset = Secret.objects.all() filterset = filters.SecretFilterSet filterset_form = forms.SecretFilterForm @@ -65,7 +63,7 @@ class SecretListView(ObjectListView): action_buttons = ('import', 'export') -class SecretView(ObjectView): +class SecretView(generic.ObjectView): queryset = Secret.objects.all() def get(self, request, pk): @@ -77,7 +75,7 @@ class SecretView(ObjectView): }) -class SecretEditView(ObjectEditView): +class SecretEditView(generic.ObjectEditView): queryset = Secret.objects.all() model_form = forms.SecretForm template_name = 'secrets/secret_edit.html' @@ -146,11 +144,11 @@ class SecretEditView(ObjectEditView): }) -class SecretDeleteView(ObjectDeleteView): +class SecretDeleteView(generic.ObjectDeleteView): queryset = Secret.objects.all() -class SecretBulkImportView(BulkImportView): +class SecretBulkImportView(generic.BulkImportView): queryset = Secret.objects.all() model_form = forms.SecretCSVForm table = tables.SecretTable @@ -197,14 +195,14 @@ class SecretBulkImportView(BulkImportView): }) -class SecretBulkEditView(BulkEditView): +class SecretBulkEditView(generic.BulkEditView): queryset = Secret.objects.prefetch_related('role') filterset = filters.SecretFilterSet table = tables.SecretTable form = forms.SecretBulkEditForm -class SecretBulkDeleteView(BulkDeleteView): +class SecretBulkDeleteView(generic.BulkDeleteView): queryset = Secret.objects.prefetch_related('role') filterset = filters.SecretFilterSet table = tables.SecretTable diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 16c801580..87d2fe620 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -1,12 +1,9 @@ -from django.db.models import Count from django.shortcuts import get_object_or_404, render from circuits.models import Circuit from dcim.models import Site, Rack, Device, RackReservation 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 . import filters, forms, tables from .models import Tenant, TenantGroup @@ -16,7 +13,7 @@ from .models import Tenant, TenantGroup # Tenant groups # -class TenantGroupListView(ObjectListView): +class TenantGroupListView(generic.ObjectListView): queryset = TenantGroup.objects.add_related_count( TenantGroup.objects.all(), Tenant, @@ -27,22 +24,22 @@ class TenantGroupListView(ObjectListView): table = tables.TenantGroupTable -class TenantGroupEditView(ObjectEditView): +class TenantGroupEditView(generic.ObjectEditView): queryset = TenantGroup.objects.all() model_form = forms.TenantGroupForm -class TenantGroupDeleteView(ObjectDeleteView): +class TenantGroupDeleteView(generic.ObjectDeleteView): queryset = TenantGroup.objects.all() -class TenantGroupBulkImportView(BulkImportView): +class TenantGroupBulkImportView(generic.BulkImportView): queryset = TenantGroup.objects.all() model_form = forms.TenantGroupCSVForm table = tables.TenantGroupTable -class TenantGroupBulkDeleteView(BulkDeleteView): +class TenantGroupBulkDeleteView(generic.BulkDeleteView): queryset = TenantGroup.objects.add_related_count( TenantGroup.objects.all(), Tenant, @@ -57,14 +54,14 @@ class TenantGroupBulkDeleteView(BulkDeleteView): # Tenants # -class TenantListView(ObjectListView): +class TenantListView(generic.ObjectListView): queryset = Tenant.objects.all() filterset = filters.TenantFilterSet filterset_form = forms.TenantFilterForm table = tables.TenantTable -class TenantView(ObjectView): +class TenantView(generic.ObjectView): queryset = Tenant.objects.prefetch_related('group') def get(self, request, slug): @@ -90,30 +87,30 @@ class TenantView(ObjectView): }) -class TenantEditView(ObjectEditView): +class TenantEditView(generic.ObjectEditView): queryset = Tenant.objects.all() model_form = forms.TenantForm template_name = 'tenancy/tenant_edit.html' -class TenantDeleteView(ObjectDeleteView): +class TenantDeleteView(generic.ObjectDeleteView): queryset = Tenant.objects.all() -class TenantBulkImportView(BulkImportView): +class TenantBulkImportView(generic.BulkImportView): queryset = Tenant.objects.all() model_form = forms.TenantCSVForm table = tables.TenantTable -class TenantBulkEditView(BulkEditView): +class TenantBulkEditView(generic.BulkEditView): queryset = Tenant.objects.prefetch_related('group') filterset = filters.TenantFilterSet table = tables.TenantTable form = forms.TenantBulkEditForm -class TenantBulkDeleteView(BulkDeleteView): +class TenantBulkDeleteView(generic.BulkDeleteView): queryset = Tenant.objects.prefetch_related('group') filterset = filters.TenantFilterSet table = tables.TenantTable diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index d328d91e4..c291a3cf2 100644 --- a/netbox/utilities/views.py +++ b/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.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.exceptions import NoReverseMatch -from django.utils.html import escape 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): @@ -152,1221 +123,3 @@ class GetReturnURLMixin: # If all else fails, return home. Ideally this should never happen. 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 = '{} {}'.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: {}'.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('

Server Error (500)

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