From 769232f368855531646cdf326cee7a79d36c1c0e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 18 May 2017 14:32:11 -0400 Subject: [PATCH 01/20] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 82c4554f0..348659e71 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ except ImportError: ) -VERSION = '2.0.3' +VERSION = '2.0.4-dev' # Import local configuration ALLOWED_HOSTS = DATABASE = SECRET_KEY = None From c454bfcd84a2d28e2f74547979d981fb547e6666 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 18 May 2017 14:53:35 -0400 Subject: [PATCH 02/20] Fixed incorrect message --- upgrade.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/upgrade.sh b/upgrade.sh index 85547277b..53e0c1db6 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -31,7 +31,7 @@ eval $COMMAND # Apply any database migrations COMMAND="./netbox/manage.py migrate" -echo "Updating required Python packages ($COMMAND)..." +echo "Applying database migrations ($COMMAND)..." eval $COMMAND # Collect static files From fb85867d72869fdc8963e902bf0e3e932ae00229 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 18 May 2017 17:00:57 -0400 Subject: [PATCH 03/20] Converted all object views to class-based views --- netbox/circuits/urls.py | 4 +- netbox/circuits/views.py | 62 ++++--- netbox/dcim/urls.py | 12 +- netbox/dcim/views.py | 373 +++++++++++++++++++++------------------ netbox/ipam/urls.py | 12 +- netbox/ipam/views.py | 349 ++++++++++++++++++++---------------- netbox/secrets/urls.py | 2 +- netbox/secrets/views.py | 15 +- netbox/tenancy/urls.py | 2 +- netbox/tenancy/views.py | 47 ++--- 10 files changed, 487 insertions(+), 391 deletions(-) diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index b23f21a88..be2791382 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -12,7 +12,7 @@ urlpatterns = [ url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'), url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), - url(r'^providers/(?P[\w-]+)/$', views.provider, name='provider'), + url(r'^providers/(?P[\w-]+)/$', views.ProviderView.as_view(), name='provider'), url(r'^providers/(?P[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'), url(r'^providers/(?P[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'), @@ -28,7 +28,7 @@ urlpatterns = [ url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'), url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'), url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'), - url(r'^circuits/(?P\d+)/$', views.circuit, name='circuit'), + url(r'^circuits/(?P\d+)/$', views.CircuitView.as_view(), name='circuit'), url(r'^circuits/(?P\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'), url(r'^circuits/(?P\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'), url(r'^circuits/(?P\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'), diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 9cc4dd3c4..d1f192030 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -5,6 +5,7 @@ from django.db import transaction from django.db.models import Count from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse +from django.views.generic import View from extras.models import Graph, GRAPH_TYPE_PROVIDER from utilities.forms import ConfirmationForm @@ -28,18 +29,23 @@ class ProviderListView(ObjectListView): template_name = 'circuits/provider_list.html' -def provider(request, slug): +class ProviderView(View): - provider = get_object_or_404(Provider, slug=slug) - circuits = Circuit.objects.filter(provider=provider).select_related('type', 'tenant')\ - .prefetch_related('terminations__site') - show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists() + def get(self, request, slug): - return render(request, 'circuits/provider.html', { - 'provider': provider, - 'circuits': circuits, - 'show_graphs': show_graphs, - }) + provider = get_object_or_404(Provider, slug=slug) + circuits = Circuit.objects.filter(provider=provider).select_related( + 'type', 'tenant' + ).prefetch_related( + 'terminations__site' + ) + show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists() + + return render(request, 'circuits/provider.html', { + 'provider': provider, + 'circuits': circuits, + 'show_graphs': show_graphs, + }) class ProviderEditView(PermissionRequiredMixin, ObjectEditView): @@ -117,25 +123,27 @@ class CircuitListView(ObjectListView): template_name = 'circuits/circuit_list.html' -def circuit(request, pk): +class CircuitView(View): - circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk) - termination_a = CircuitTermination.objects.select_related( - 'site__region', 'interface__device' - ).filter( - circuit=circuit, term_side=TERM_SIDE_A - ).first() - termination_z = CircuitTermination.objects.select_related( - 'site__region', 'interface__device' - ).filter( - circuit=circuit, term_side=TERM_SIDE_Z - ).first() + def get(self, request, pk): - return render(request, 'circuits/circuit.html', { - 'circuit': circuit, - 'termination_a': termination_a, - 'termination_z': termination_z, - }) + circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk) + termination_a = CircuitTermination.objects.select_related( + 'site__region', 'interface__device' + ).filter( + circuit=circuit, term_side=TERM_SIDE_A + ).first() + termination_z = CircuitTermination.objects.select_related( + 'site__region', 'interface__device' + ).filter( + circuit=circuit, term_side=TERM_SIDE_Z + ).first() + + return render(request, 'circuits/circuit.html', { + 'circuit': circuit, + 'termination_a': termination_a, + 'termination_z': termination_z, + }) class CircuitEditView(PermissionRequiredMixin, ObjectEditView): diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 8e666f406..bfe74dec6 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -22,7 +22,7 @@ urlpatterns = [ url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'), url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'), url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'), - url(r'^sites/(?P[\w-]+)/$', views.site, name='site'), + url(r'^sites/(?P[\w-]+)/$', views.SiteView.as_view(), name='site'), url(r'^sites/(?P[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'), url(r'^sites/(?P[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'), url(r'^sites/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}), @@ -52,7 +52,7 @@ urlpatterns = [ url(r'^racks/import/$', views.RackBulkImportView.as_view(), name='rack_import'), url(r'^racks/edit/$', views.RackBulkEditView.as_view(), name='rack_bulk_edit'), url(r'^racks/delete/$', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'), - url(r'^racks/(?P\d+)/$', views.rack, name='rack'), + url(r'^racks/(?P\d+)/$', views.RackView.as_view(), name='rack'), url(r'^racks/(?P\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'), url(r'^racks/(?P\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'), url(r'^racks/(?P\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'), @@ -69,7 +69,7 @@ urlpatterns = [ url(r'^device-types/add/$', views.DeviceTypeEditView.as_view(), name='devicetype_add'), url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), - url(r'^device-types/(?P\d+)/$', views.devicetype, name='devicetype'), + url(r'^device-types/(?P\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'), url(r'^device-types/(?P\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), url(r'^device-types/(?P\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), @@ -117,11 +117,11 @@ urlpatterns = [ url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'), url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'), url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'), - url(r'^devices/(?P\d+)/$', views.device, name='device'), + url(r'^devices/(?P\d+)/$', views.DeviceView.as_view(), name='device'), url(r'^devices/(?P\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'), url(r'^devices/(?P\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'), - url(r'^devices/(?P\d+)/inventory/$', views.device_inventory, name='device_inventory'), - url(r'^devices/(?P\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'), + url(r'^devices/(?P\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'), + url(r'^devices/(?P\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), url(r'^devices/(?P\d+)/add-secret/$', secret_add, name='device_addsecret'), url(r'^devices/(?P\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'), url(r'^devices/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 4ffe2d36b..0e3028e49 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -178,27 +178,29 @@ class SiteListView(ObjectListView): template_name = 'dcim/site_list.html' -def site(request, slug): +class SiteView(View): - site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug) - stats = { - 'rack_count': Rack.objects.filter(site=site).count(), - 'device_count': Device.objects.filter(site=site).count(), - 'prefix_count': Prefix.objects.filter(site=site).count(), - 'vlan_count': VLAN.objects.filter(site=site).count(), - 'circuit_count': Circuit.objects.filter(terminations__site=site).count(), - } - rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks')) - topology_maps = TopologyMap.objects.filter(site=site) - show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists() + def get(self, request, slug): - return render(request, 'dcim/site.html', { - 'site': site, - 'stats': stats, - 'rack_groups': rack_groups, - 'topology_maps': topology_maps, - 'show_graphs': show_graphs, - }) + site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug) + stats = { + 'rack_count': Rack.objects.filter(site=site).count(), + 'device_count': Device.objects.filter(site=site).count(), + 'prefix_count': Prefix.objects.filter(site=site).count(), + 'vlan_count': VLAN.objects.filter(site=site).count(), + 'circuit_count': Circuit.objects.filter(terminations__site=site).count(), + } + rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks')) + topology_maps = TopologyMap.objects.filter(site=site) + show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists() + + return render(request, 'dcim/site.html', { + 'site': site, + 'stats': stats, + 'rack_groups': rack_groups, + 'topology_maps': topology_maps, + 'show_graphs': show_graphs, + }) class SiteEditView(PermissionRequiredMixin, ObjectEditView): @@ -290,8 +292,13 @@ class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # class RackListView(ObjectListView): - queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('devices__device_type')\ - .annotate(device_count=Count('devices', distinct=True)) + queryset = Rack.objects.select_related( + 'site', 'group', 'tenant', 'role' + ).prefetch_related( + 'devices__device_type' + ).annotate( + device_count=Count('devices', distinct=True) + ) filter = filters.RackFilter filter_form = forms.RackFilterForm table = tables.RackTable @@ -338,31 +345,33 @@ class RackElevationListView(View): }) -def rack(request, pk): +class RackView(View): - rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk) + def get(self, request, pk): - nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\ - .select_related('device_type__manufacturer') - next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first() - prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first() + rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk) - reservations = RackReservation.objects.filter(rack=rack) - reserved_units = {} - for r in reservations: - for u in r.units: - reserved_units[u] = r + nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\ + .select_related('device_type__manufacturer') + next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first() + prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first() - return render(request, 'dcim/rack.html', { - 'rack': rack, - 'reservations': reservations, - 'reserved_units': reserved_units, - 'nonracked_devices': nonracked_devices, - 'next_rack': next_rack, - 'prev_rack': prev_rack, - 'front_elevation': rack.get_front_elevation(), - 'rear_elevation': rack.get_rear_elevation(), - }) + reservations = RackReservation.objects.filter(rack=rack) + reserved_units = {} + for r in reservations: + for u in r.units: + reserved_units[u] = r + + return render(request, 'dcim/rack.html', { + 'rack': rack, + 'reservations': reservations, + 'reserved_units': reserved_units, + 'nonracked_devices': nonracked_devices, + 'next_rack': next_rack, + 'prev_rack': prev_rack, + 'front_elevation': rack.get_front_elevation(), + 'rear_elevation': rack.get_rear_elevation(), + }) class RackEditView(PermissionRequiredMixin, ObjectEditView): @@ -481,53 +490,57 @@ class DeviceTypeListView(ObjectListView): template_name = 'dcim/devicetype_list.html' -def devicetype(request, pk): +class DeviceTypeView(View): - devicetype = get_object_or_404(DeviceType, pk=pk) + def get(self, request, pk): - # Component tables - consoleport_table = tables.ConsolePortTemplateTable( - natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) - ) - consoleserverport_table = tables.ConsoleServerPortTemplateTable( - natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) - ) - powerport_table = tables.PowerPortTemplateTable( - natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) - ) - poweroutlet_table = tables.PowerOutletTemplateTable( - natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) - ) - mgmt_interface_table = tables.InterfaceTemplateTable( - list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype, - mgmt_only=True)) - ) - interface_table = tables.InterfaceTemplateTable( - list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype, - mgmt_only=False)) - ) - devicebay_table = tables.DeviceBayTemplateTable( - natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) - ) - if request.user.has_perm('dcim.change_devicetype'): - consoleport_table.base_columns['pk'].visible = True - consoleserverport_table.base_columns['pk'].visible = True - powerport_table.base_columns['pk'].visible = True - poweroutlet_table.base_columns['pk'].visible = True - mgmt_interface_table.base_columns['pk'].visible = True - interface_table.base_columns['pk'].visible = True - devicebay_table.base_columns['pk'].visible = True + devicetype = get_object_or_404(DeviceType, pk=pk) - return render(request, 'dcim/devicetype.html', { - 'devicetype': devicetype, - 'consoleport_table': consoleport_table, - 'consoleserverport_table': consoleserverport_table, - 'powerport_table': powerport_table, - 'poweroutlet_table': poweroutlet_table, - 'mgmt_interface_table': mgmt_interface_table, - 'interface_table': interface_table, - 'devicebay_table': devicebay_table, - }) + # Component tables + consoleport_table = tables.ConsolePortTemplateTable( + natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) + consoleserverport_table = tables.ConsoleServerPortTemplateTable( + natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) + powerport_table = tables.PowerPortTemplateTable( + natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) + poweroutlet_table = tables.PowerOutletTemplateTable( + natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) + mgmt_interface_table = tables.InterfaceTemplateTable( + list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter( + device_type=devicetype, mgmt_only=True + )) + ) + interface_table = tables.InterfaceTemplateTable( + list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter( + device_type=devicetype, mgmt_only=False + )) + ) + devicebay_table = tables.DeviceBayTemplateTable( + natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) + if request.user.has_perm('dcim.change_devicetype'): + consoleport_table.base_columns['pk'].visible = True + consoleserverport_table.base_columns['pk'].visible = True + powerport_table.base_columns['pk'].visible = True + poweroutlet_table.base_columns['pk'].visible = True + mgmt_interface_table.base_columns['pk'].visible = True + interface_table.base_columns['pk'].visible = True + devicebay_table.base_columns['pk'].visible = True + + return render(request, 'dcim/devicetype.html', { + 'devicetype': devicetype, + 'consoleport_table': consoleport_table, + 'consoleserverport_table': consoleserverport_table, + 'powerport_table': powerport_table, + 'poweroutlet_table': poweroutlet_table, + 'mgmt_interface_table': mgmt_interface_table, + 'interface_table': interface_table, + 'devicebay_table': devicebay_table, + }) class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView): @@ -727,70 +740,114 @@ class DeviceListView(ObjectListView): template_name = 'dcim/device_list.html' -def device(request, pk): +class DeviceView(View): - device = get_object_or_404(Device.objects.select_related( - 'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform' - ), pk=pk) - console_ports = natsorted( - ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name') - ) - cs_ports = natsorted( - ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name') - ) - power_ports = natsorted( - PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name') - ) - power_outlets = natsorted( - PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') - ) - interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ - .filter(device=device, mgmt_only=False)\ - .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', - 'circuit_termination__circuit').prefetch_related('ip_addresses') - mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ - .filter(device=device, mgmt_only=True)\ - .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', - 'circuit_termination__circuit').prefetch_related('ip_addresses') - device_bays = natsorted( - DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), - key=attrgetter('name') - ) - services = Service.objects.filter(device=device) - secrets = device.secrets.all() + def get(self, request, pk): - # Find any related devices for convenient linking in the UI - related_devices = [] - if device.name: - if re.match('.+[0-9]+$', device.name): - # Strip 1 or more trailing digits (e.g. core-switch1) - base_name = re.match('(.*?)[0-9]+$', device.name).group(1) - elif re.match('.+\d[a-z]$', device.name.lower()): - # Strip a trailing letter if preceded by a digit (e.g. dist-switch3a -> dist-switch3) - base_name = re.match('(.*\d+)[a-z]$', device.name.lower()).group(1) - else: - base_name = None - if base_name: - related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\ - .select_related('rack', 'device_type__manufacturer')[:10] + device = get_object_or_404(Device.objects.select_related( + 'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform' + ), pk=pk) + console_ports = natsorted( + ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name') + ) + cs_ports = natsorted( + ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name') + ) + power_ports = natsorted( + PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name') + ) + power_outlets = natsorted( + PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') + ) + interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter( + device=device, mgmt_only=False + ).select_related( + 'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', + 'circuit_termination__circuit' + ).prefetch_related('ip_addresses') + mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter( + device=device, mgmt_only=True + ).select_related( + 'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', + 'circuit_termination__circuit' + ).prefetch_related('ip_addresses') + device_bays = natsorted( + DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), + key=attrgetter('name') + ) + services = Service.objects.filter(device=device) + secrets = device.secrets.all() - # Show graph button on interfaces only if at least one graph has been created. - show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists() + # Find any related devices for convenient linking in the UI + related_devices = [] + if device.name: + if re.match('.+[0-9]+$', device.name): + # Strip 1 or more trailing digits (e.g. core-switch1) + base_name = re.match('(.*?)[0-9]+$', device.name).group(1) + elif re.match('.+\d[a-z]$', device.name.lower()): + # Strip a trailing letter if preceded by a digit (e.g. dist-switch3a -> dist-switch3) + base_name = re.match('(.*\d+)[a-z]$', device.name.lower()).group(1) + else: + base_name = None + if base_name: + related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\ + .select_related('rack', 'device_type__manufacturer')[:10] - return render(request, 'dcim/device.html', { - 'device': device, - 'console_ports': console_ports, - 'cs_ports': cs_ports, - 'power_ports': power_ports, - 'power_outlets': power_outlets, - 'interfaces': interfaces, - 'mgmt_interfaces': mgmt_interfaces, - 'device_bays': device_bays, - 'services': services, - 'secrets': secrets, - 'related_devices': related_devices, - 'show_graphs': show_graphs, - }) + # Show graph button on interfaces only if at least one graph has been created. + show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists() + + return render(request, 'dcim/device.html', { + 'device': device, + 'console_ports': console_ports, + 'cs_ports': cs_ports, + 'power_ports': power_ports, + 'power_outlets': power_outlets, + 'interfaces': interfaces, + 'mgmt_interfaces': mgmt_interfaces, + 'device_bays': device_bays, + 'services': services, + 'secrets': secrets, + 'related_devices': related_devices, + 'show_graphs': show_graphs, + }) + + +class DeviceInventoryView(View): + + def get(self, request, pk): + + device = get_object_or_404(Device, pk=pk) + inventory_items = InventoryItem.objects.filter( + device=device, parent=None + ).select_related( + 'manufacturer' + ).prefetch_related( + 'child_items' + ) + + return render(request, 'dcim/device_inventory.html', { + 'device': device, + 'inventory_items': inventory_items, + }) + + +class DeviceLLDPNeighborsView(View): + + def get(self, request, pk): + + device = get_object_or_404(Device, pk=pk) + interfaces = Interface.objects.order_naturally( + device.device_type.interface_ordering + ).filter( + device=device + ).select_related( + 'connected_as_a', 'connected_as_b' + ) + + return render(request, 'dcim/device_lldp_neighbors.html', { + 'device': device, + 'interfaces': interfaces, + }) class DeviceEditView(PermissionRequiredMixin, ObjectEditView): @@ -851,30 +908,6 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): default_return_url = 'dcim:device_list' -def device_inventory(request, pk): - - device = get_object_or_404(Device, pk=pk) - inventory_items = InventoryItem.objects.filter(device=device, parent=None).select_related('manufacturer')\ - .prefetch_related('child_items') - - return render(request, 'dcim/device_inventory.html', { - 'device': device, - 'inventory_items': inventory_items, - }) - - -def device_lldp_neighbors(request, pk): - - device = get_object_or_404(Device, pk=pk) - interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\ - .select_related('connected_as_a', 'connected_as_b') - - return render(request, 'dcim/device_lldp_neighbors.html', { - 'device': device, - 'interfaces': interfaces, - }) - - # # Console ports # diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index a8d7d4528..59b1d1fd3 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -12,7 +12,7 @@ urlpatterns = [ url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'), url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'), url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'), - url(r'^vrfs/(?P\d+)/$', views.vrf, name='vrf'), + url(r'^vrfs/(?P\d+)/$', views.VRFView.as_view(), name='vrf'), url(r'^vrfs/(?P\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'), url(r'^vrfs/(?P\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'), @@ -28,7 +28,7 @@ urlpatterns = [ url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'), url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'), url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'), - url(r'^aggregates/(?P\d+)/$', views.aggregate, name='aggregate'), + url(r'^aggregates/(?P\d+)/$', views.AggregateView.as_view(), name='aggregate'), url(r'^aggregates/(?P\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'), url(r'^aggregates/(?P\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'), @@ -44,10 +44,10 @@ urlpatterns = [ url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'), url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'), url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'), - url(r'^prefixes/(?P\d+)/$', views.prefix, name='prefix'), + url(r'^prefixes/(?P\d+)/$', views.PrefixView.as_view(), name='prefix'), url(r'^prefixes/(?P\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'), url(r'^prefixes/(?P\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'), - url(r'^prefixes/(?P\d+)/ip-addresses/$', views.prefix_ipaddresses, name='prefix_ipaddresses'), + url(r'^prefixes/(?P\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'), # IP addresses url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'), @@ -56,7 +56,7 @@ urlpatterns = [ url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'), url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), - url(r'^ip-addresses/(?P\d+)/$', views.ipaddress, name='ipaddress'), + url(r'^ip-addresses/(?P\d+)/$', views.IPAddressView.as_view(), name='ipaddress'), url(r'^ip-addresses/(?P\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'), url(r'^ip-addresses/(?P\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), @@ -72,7 +72,7 @@ urlpatterns = [ url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'), url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'), url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'), - url(r'^vlans/(?P\d+)/$', views.vlan, name='vlan'), + url(r'^vlans/(?P\d+)/$', views.VLANView.as_view(), name='vlan'), url(r'^vlans/(?P\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'), url(r'^vlans/(?P\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 82e5f8331..b49db4250 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -6,6 +6,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.db.models import Count, Q from django.shortcuts import get_object_or_404, render from django.urls import reverse +from django.views.generic import View from dcim.models import Device from utilities.paginator import EnhancedPaginator @@ -96,18 +97,20 @@ class VRFListView(ObjectListView): template_name = 'ipam/vrf_list.html' -def vrf(request, pk): +class VRFView(View): - vrf = get_object_or_404(VRF.objects.all(), pk=pk) - prefix_table = tables.PrefixBriefTable( - list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')) - ) - prefix_table.exclude = ('vrf',) + def get(self, request, pk): - return render(request, 'ipam/vrf.html', { - 'vrf': vrf, - 'prefix_table': prefix_table, - }) + vrf = get_object_or_404(VRF.objects.all(), pk=pk) + prefix_table = tables.PrefixBriefTable( + list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')) + ) + prefix_table.exclude = ('vrf',) + + return render(request, 'ipam/vrf.html', { + 'vrf': vrf, + 'prefix_table': prefix_table, + }) class VRFEditView(PermissionRequiredMixin, ObjectEditView): @@ -281,37 +284,44 @@ class AggregateListView(ObjectListView): } -def aggregate(request, pk): +class AggregateView(View): - aggregate = get_object_or_404(Aggregate, pk=pk) + def get(self, request, pk): - # Find all child prefixes contained by this aggregate - child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))\ - .select_related('site', 'role').annotate_depth(limit=0) - child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes) + aggregate = get_object_or_404(Aggregate, pk=pk) - prefix_table = tables.PrefixTable(child_prefixes) - if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): - prefix_table.base_columns['pk'].visible = True + # Find all child prefixes contained by this aggregate + child_prefixes = Prefix.objects.filter( + prefix__net_contained_or_equal=str(aggregate.prefix) + ).select_related( + 'site', 'role' + ).annotate_depth( + limit=0 + ) + child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes) - paginate = { - 'klass': EnhancedPaginator, - 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) - } - RequestConfig(request, paginate).configure(prefix_table) + prefix_table = tables.PrefixTable(child_prefixes) + if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): + prefix_table.base_columns['pk'].visible = True - # Compile permissions list for rendering the object table - permissions = { - 'add': request.user.has_perm('ipam.add_prefix'), - 'change': request.user.has_perm('ipam.change_prefix'), - 'delete': request.user.has_perm('ipam.delete_prefix'), - } + paginate = { + 'klass': EnhancedPaginator, + 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) + } + RequestConfig(request, paginate).configure(prefix_table) - return render(request, 'ipam/aggregate.html', { - 'aggregate': aggregate, - 'prefix_table': prefix_table, - 'permissions': permissions, - }) + # Compile permissions list for rendering the object table + permissions = { + 'add': request.user.has_perm('ipam.add_prefix'), + 'change': request.user.has_perm('ipam.change_prefix'), + 'delete': request.user.has_perm('ipam.delete_prefix'), + } + + return render(request, 'ipam/aggregate.html', { + 'aggregate': aggregate, + 'prefix_table': prefix_table, + 'permissions': permissions, + }) class AggregateEditView(PermissionRequiredMixin, ObjectEditView): @@ -394,66 +404,120 @@ class PrefixListView(ObjectListView): return self.queryset.annotate_depth(limit=limit) -def prefix(request, pk): +class PrefixView(View): - prefix = get_object_or_404(Prefix.objects.select_related( - 'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role' - ), pk=pk) + def get(self, request, pk): - try: - aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix)) - except Aggregate.DoesNotExist: - aggregate = None + prefix = get_object_or_404(Prefix.objects.select_related( + 'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role' + ), pk=pk) - # Count child IP addresses - ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\ - .count() + try: + aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix)) + except Aggregate.DoesNotExist: + aggregate = None - # Parent prefixes table - parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\ - .filter(prefix__net_contains=str(prefix.prefix))\ - .select_related('site', 'role').annotate_depth() - parent_prefix_table = tables.PrefixBriefTable(parent_prefixes) - parent_prefix_table.exclude = ('vrf',) + # Count child IP addresses + ipaddress_count = IPAddress.objects.filter( + vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix) + ).count() - # Duplicate prefixes table - duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\ - .select_related('site', 'role') - duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes)) - duplicate_prefix_table.exclude = ('vrf',) + # Parent prefixes table + parent_prefixes = Prefix.objects.filter( + Q(vrf=prefix.vrf) | Q(vrf__isnull=True) + ).filter( + prefix__net_contains=str(prefix.prefix) + ).select_related( + 'site', 'role' + ).annotate_depth() + parent_prefix_table = tables.PrefixBriefTable(parent_prefixes) + parent_prefix_table.exclude = ('vrf',) - # Child prefixes table - child_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix))\ - .select_related('site', 'role').annotate_depth(limit=0) - if child_prefixes: - child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes) - child_prefix_table = tables.PrefixTable(child_prefixes) - if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): - child_prefix_table.base_columns['pk'].visible = True + # Duplicate prefixes table + duplicate_prefixes = Prefix.objects.filter( + vrf=prefix.vrf, prefix=str(prefix.prefix) + ).exclude( + pk=prefix.pk + ).select_related( + 'site', 'role' + ) + duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes)) + duplicate_prefix_table.exclude = ('vrf',) - paginate = { - 'klass': EnhancedPaginator, - 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) - } - RequestConfig(request, paginate).configure(child_prefix_table) + # Child prefixes table + child_prefixes = Prefix.objects.filter( + vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix) + ).select_related( + 'site', 'role' + ).annotate_depth(limit=0) + if child_prefixes: + child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes) + child_prefix_table = tables.PrefixTable(child_prefixes) + if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): + child_prefix_table.base_columns['pk'].visible = True - # Compile permissions list for rendering the object table - permissions = { - 'add': request.user.has_perm('ipam.add_prefix'), - 'change': request.user.has_perm('ipam.change_prefix'), - 'delete': request.user.has_perm('ipam.delete_prefix'), - } + paginate = { + 'klass': EnhancedPaginator, + 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) + } + RequestConfig(request, paginate).configure(child_prefix_table) - return render(request, 'ipam/prefix.html', { - 'prefix': prefix, - 'aggregate': aggregate, - 'ipaddress_count': ipaddress_count, - 'parent_prefix_table': parent_prefix_table, - 'child_prefix_table': child_prefix_table, - 'duplicate_prefix_table': duplicate_prefix_table, - 'permissions': permissions, - 'return_url': prefix.get_absolute_url(), - }) + # Compile permissions list for rendering the object table + permissions = { + 'add': request.user.has_perm('ipam.add_prefix'), + 'change': request.user.has_perm('ipam.change_prefix'), + 'delete': request.user.has_perm('ipam.delete_prefix'), + } + + return render(request, 'ipam/prefix.html', { + 'prefix': prefix, + 'aggregate': aggregate, + 'ipaddress_count': ipaddress_count, + 'parent_prefix_table': parent_prefix_table, + 'child_prefix_table': child_prefix_table, + 'duplicate_prefix_table': duplicate_prefix_table, + 'permissions': permissions, + 'return_url': prefix.get_absolute_url(), + }) + + +class PrefixIPAddressesView(View): + + def get(self, request, pk): + + prefix = get_object_or_404(Prefix.objects.all(), pk=pk) + + # Find all IPAddresses belonging to this Prefix + ipaddresses = IPAddress.objects.filter( + vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix) + ).select_related( + 'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for' + ) + ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool) + + ip_table = tables.IPAddressTable(ipaddresses) + if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'): + ip_table.base_columns['pk'].visible = True + + paginate = { + 'klass': EnhancedPaginator, + 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) + } + RequestConfig(request, paginate).configure(ip_table) + + # Compile permissions list for rendering the object table + permissions = { + 'add': request.user.has_perm('ipam.add_ipaddress'), + 'change': request.user.has_perm('ipam.change_ipaddress'), + 'delete': request.user.has_perm('ipam.delete_ipaddress'), + } + + return render(request, 'ipam/prefix_ipaddresses.html', { + 'prefix': prefix, + 'ip_table': ip_table, + 'permissions': permissions, + 'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix), + }) class PrefixEditView(PermissionRequiredMixin, ObjectEditView): @@ -495,40 +559,6 @@ class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): default_return_url = 'ipam:prefix_list' -def prefix_ipaddresses(request, pk): - - prefix = get_object_or_404(Prefix.objects.all(), pk=pk) - - # Find all IPAddresses belonging to this Prefix - ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\ - .select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for') - ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool) - - ip_table = tables.IPAddressTable(ipaddresses) - if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'): - ip_table.base_columns['pk'].visible = True - - paginate = { - 'klass': EnhancedPaginator, - 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) - } - RequestConfig(request, paginate).configure(ip_table) - - # Compile permissions list for rendering the object table - permissions = { - 'add': request.user.has_perm('ipam.add_ipaddress'), - 'change': request.user.has_perm('ipam.change_ipaddress'), - 'delete': request.user.has_perm('ipam.delete_ipaddress'), - } - - return render(request, 'ipam/prefix_ipaddresses.html', { - 'prefix': prefix, - 'ip_table': ip_table, - 'permissions': permissions, - 'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix), - }) - - # # IP addresses # @@ -541,32 +571,47 @@ class IPAddressListView(ObjectListView): template_name = 'ipam/ipaddress_list.html' -def ipaddress(request, pk): +class IPAddressView(View): - ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk) + def get(self, request, pk): - # Parent prefixes table - parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip))\ - .select_related('site', 'role') - parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes)) - parent_prefixes_table.exclude = ('vrf',) + ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk) - # Duplicate IPs table - duplicate_ips = IPAddress.objects.filter(vrf=ipaddress.vrf, address=str(ipaddress.address))\ - .exclude(pk=ipaddress.pk).select_related('interface__device', 'nat_inside') - duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips)) + # Parent prefixes table + parent_prefixes = Prefix.objects.filter( + vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip) + ).select_related( + 'site', 'role' + ) + parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes)) + parent_prefixes_table.exclude = ('vrf',) - # Related IP table - related_ips = IPAddress.objects.select_related('interface__device').exclude(address=str(ipaddress.address))\ - .filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)) - related_ips_table = tables.IPAddressBriefTable(list(related_ips)) + # Duplicate IPs table + duplicate_ips = IPAddress.objects.filter( + vrf=ipaddress.vrf, address=str(ipaddress.address) + ).exclude( + pk=ipaddress.pk + ).select_related( + 'interface__device', 'nat_inside' + ) + duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips)) - return render(request, 'ipam/ipaddress.html', { - 'ipaddress': ipaddress, - 'parent_prefixes_table': parent_prefixes_table, - 'duplicate_ips_table': duplicate_ips_table, - 'related_ips_table': related_ips_table, - }) + # Related IP table + related_ips = IPAddress.objects.select_related( + 'interface__device' + ).exclude( + address=str(ipaddress.address) + ).filter( + vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address) + ) + related_ips_table = tables.IPAddressBriefTable(list(related_ips)) + + return render(request, 'ipam/ipaddress.html', { + 'ipaddress': ipaddress, + 'parent_prefixes_table': parent_prefixes_table, + 'duplicate_ips_table': duplicate_ips_table, + 'related_ips_table': related_ips_table, + }) class IPAddressEditView(PermissionRequiredMixin, ObjectEditView): @@ -669,17 +714,21 @@ class VLANListView(ObjectListView): template_name = 'ipam/vlan_list.html' -def vlan(request, pk): +class VLANView(View): - vlan = get_object_or_404(VLAN.objects.select_related('site__region', 'tenant__group', 'role'), pk=pk) - prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role') - prefix_table = tables.PrefixBriefTable(list(prefixes)) - prefix_table.exclude = ('vlan',) + def get(self, request, pk): - return render(request, 'ipam/vlan.html', { - 'vlan': vlan, - 'prefix_table': prefix_table, - }) + vlan = get_object_or_404(VLAN.objects.select_related( + 'site__region', 'tenant__group', 'role' + ), pk=pk) + prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role') + prefix_table = tables.PrefixBriefTable(list(prefixes)) + prefix_table.exclude = ('vlan',) + + return render(request, 'ipam/vlan.html', { + 'vlan': vlan, + 'prefix_table': prefix_table, + }) class VLANEditView(PermissionRequiredMixin, ObjectEditView): diff --git a/netbox/secrets/urls.py b/netbox/secrets/urls.py index a4d3b7a78..e801c2de2 100644 --- a/netbox/secrets/urls.py +++ b/netbox/secrets/urls.py @@ -17,7 +17,7 @@ urlpatterns = [ url(r'^secrets/import/$', views.secret_import, name='secret_import'), url(r'^secrets/edit/$', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'), url(r'^secrets/delete/$', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'), - url(r'^secrets/(?P\d+)/$', views.secret, name='secret'), + url(r'^secrets/(?P\d+)/$', views.SecretView.as_view(), name='secret'), url(r'^secrets/(?P\d+)/edit/$', views.secret_edit, name='secret_edit'), url(r'^secrets/(?P\d+)/delete/$', views.SecretDeleteView.as_view(), name='secret_delete'), diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 0a132dca3..05fb5c262 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -8,6 +8,7 @@ from django.db.models import Count from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.decorators import method_decorator +from django.views.generic import View from dcim.models import Device from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView @@ -65,14 +66,16 @@ class SecretListView(ObjectListView): template_name = 'secrets/secret_list.html' -@login_required -def secret(request, pk): +@method_decorator(login_required, name='dispatch') +class SecretView(View): - secret = get_object_or_404(Secret, pk=pk) + def get(self, request, pk): - return render(request, 'secrets/secret.html', { - 'secret': secret, - }) + secret = get_object_or_404(Secret, pk=pk) + + return render(request, 'secrets/secret.html', { + 'secret': secret, + }) @permission_required('secrets.add_secret') diff --git a/netbox/tenancy/urls.py b/netbox/tenancy/urls.py index b3ca2ecc2..be450be7e 100644 --- a/netbox/tenancy/urls.py +++ b/netbox/tenancy/urls.py @@ -18,7 +18,7 @@ urlpatterns = [ url(r'^tenants/import/$', views.TenantBulkImportView.as_view(), name='tenant_import'), url(r'^tenants/edit/$', views.TenantBulkEditView.as_view(), name='tenant_bulk_edit'), url(r'^tenants/delete/$', views.TenantBulkDeleteView.as_view(), name='tenant_bulk_delete'), - url(r'^tenants/(?P[\w-]+)/$', views.tenant, name='tenant'), + url(r'^tenants/(?P[\w-]+)/$', views.TenantView.as_view(), name='tenant'), url(r'^tenants/(?P[\w-]+)/edit/$', views.TenantEditView.as_view(), name='tenant_edit'), url(r'^tenants/(?P[\w-]+)/delete/$', views.TenantDeleteView.as_view(), name='tenant_delete'), diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 97ed0eb01..d520039b4 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -2,6 +2,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.db.models import Count, Q from django.shortcuts import get_object_or_404, render from django.urls import reverse +from django.views.generic import View from circuits.models import Circuit from dcim.models import Site, Rack, Device @@ -51,30 +52,32 @@ class TenantListView(ObjectListView): template_name = 'tenancy/tenant_list.html' -def tenant(request, slug): +class TenantView(View): - tenant = get_object_or_404(Tenant, slug=slug) - stats = { - 'site_count': Site.objects.filter(tenant=tenant).count(), - 'rack_count': Rack.objects.filter(tenant=tenant).count(), - 'device_count': Device.objects.filter(tenant=tenant).count(), - 'vrf_count': VRF.objects.filter(tenant=tenant).count(), - 'prefix_count': Prefix.objects.filter( - Q(tenant=tenant) | - Q(tenant__isnull=True, vrf__tenant=tenant) - ).count(), - 'ipaddress_count': IPAddress.objects.filter( - Q(tenant=tenant) | - Q(tenant__isnull=True, vrf__tenant=tenant) - ).count(), - 'vlan_count': VLAN.objects.filter(tenant=tenant).count(), - 'circuit_count': Circuit.objects.filter(tenant=tenant).count(), - } + def get(self, request, slug): - return render(request, 'tenancy/tenant.html', { - 'tenant': tenant, - 'stats': stats, - }) + tenant = get_object_or_404(Tenant, slug=slug) + stats = { + 'site_count': Site.objects.filter(tenant=tenant).count(), + 'rack_count': Rack.objects.filter(tenant=tenant).count(), + 'device_count': Device.objects.filter(tenant=tenant).count(), + 'vrf_count': VRF.objects.filter(tenant=tenant).count(), + 'prefix_count': Prefix.objects.filter( + Q(tenant=tenant) | + Q(tenant__isnull=True, vrf__tenant=tenant) + ).count(), + 'ipaddress_count': IPAddress.objects.filter( + Q(tenant=tenant) | + Q(tenant__isnull=True, vrf__tenant=tenant) + ).count(), + 'vlan_count': VLAN.objects.filter(tenant=tenant).count(), + 'circuit_count': Circuit.objects.filter(tenant=tenant).count(), + } + + return render(request, 'tenancy/tenant.html', { + 'tenant': tenant, + 'stats': stats, + }) class TenantEditView(PermissionRequiredMixin, ObjectEditView): From eece8a0e26c2d517fe6e0808169843e8c48f86e8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 19 May 2017 12:59:27 -0400 Subject: [PATCH 04/20] Fixes #1207: Include nested LAG serializer when showing interface connections (API) --- netbox/dcim/api/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 2959aa901..4c9c10804 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -618,10 +618,11 @@ class PeerInterfaceSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') device = NestedDeviceSerializer() form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES) + lag = NestedInterfaceSerializer() class Meta: model = Interface - fields = ['id', 'url', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description'] + fields = ['id', 'url', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description'] class WritableInterfaceSerializer(serializers.ModelSerializer): From 27c94d9874878c1f31ac376accbc17b1ccf45f15 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 19 May 2017 13:23:17 -0400 Subject: [PATCH 05/20] Fixes #1206: Fix redirection in admin UI after activating secret keys when BASE_PATH is set --- netbox/secrets/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/secrets/admin.py b/netbox/secrets/admin.py index 990f3ffc7..58658a07e 100644 --- a/netbox/secrets/admin.py +++ b/netbox/secrets/admin.py @@ -35,7 +35,7 @@ class UserKeyAdmin(admin.ModelAdmin): my_userkey = UserKey.objects.get(user=request.user) except UserKey.DoesNotExist: messages.error(request, u"You do not have an active User Key.") - return redirect('/admin/secrets/userkey/') + return redirect('admin:secrets_userkey_changelist') if 'activate' in request.POST: form = ActivateUserKeyForm(request.POST) @@ -44,7 +44,7 @@ class UserKeyAdmin(admin.ModelAdmin): master_key = my_userkey.get_master_key(form.cleaned_data['secret_key']) for uk in form.cleaned_data['_selected_action']: uk.activate(master_key) - return redirect('/admin/secrets/userkey/') + return redirect('admin:secrets_userkey_changelist') except ValueError: messages.error(request, u"Invalid private key provided. Unable to retrieve master key.") else: From e6b4d8793960d9b3047a41e885a2db1038f77797 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 19 May 2017 15:47:19 -0400 Subject: [PATCH 06/20] Converted all user views to CBVs --- netbox/netbox/urls.py | 6 +- netbox/users/urls.py | 10 +-- netbox/users/views.py | 151 +++++++++++++++++++++++++----------------- 3 files changed, 100 insertions(+), 67 deletions(-) diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 7e3ab8924..a1e7eb351 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -6,7 +6,7 @@ from django.contrib import admin from django.views.static import serve from netbox.views import APIRootView, home, handle_500, SearchView, trigger_500 -from users.views import login, logout +from users.views import LoginView, LogoutView handler500 = handle_500 @@ -19,8 +19,8 @@ _patterns = [ url(r'^search/$', SearchView.as_view(), name='search'), # Login/logout - url(r'^login/$', login, name='login'), - url(r'^logout/$', logout, name='logout'), + url(r'^login/$', LoginView.as_view(), name='login'), + url(r'^logout/$', LogoutView.as_view(), name='logout'), # Apps url(r'^circuits/', include('circuits.urls')), diff --git a/netbox/users/urls.py b/netbox/users/urls.py index ab714f49b..3183e5063 100644 --- a/netbox/users/urls.py +++ b/netbox/users/urls.py @@ -6,15 +6,15 @@ from . import views app_name = 'user' urlpatterns = [ - url(r'^profile/$', views.profile, name='profile'), - url(r'^password/$', views.change_password, name='change_password'), + url(r'^profile/$', views.ProfileView.as_view(), name='profile'), + url(r'^password/$', views.ChangePasswordView.as_view(), name='change_password'), url(r'^api-tokens/$', views.TokenListView.as_view(), name='token_list'), url(r'^api-tokens/add/$', views.TokenEditView.as_view(), name='token_add'), url(r'^api-tokens/(?P\d+)/edit/$', views.TokenEditView.as_view(), name='token_edit'), url(r'^api-tokens/(?P\d+)/delete/$', views.TokenDeleteView.as_view(), name='token_delete'), - url(r'^user-key/$', views.userkey, name='userkey'), - url(r'^user-key/edit/$', views.userkey_edit, name='userkey_edit'), + url(r'^user-key/$', views.UserKeyView.as_view(), name='userkey'), + url(r'^user-key/edit/$', views.UserKeyEditView.as_view(), name='userkey_edit'), url(r'^session-key/delete/$', views.SessionKeyDeleteView.as_view(), name='sessionkey_delete'), - url(r'^recent-activity/$', views.recent_activity, name='recent_activity'), + url(r'^recent-activity/$', views.RecentActivityView.as_view(), name='recent_activity'), ] diff --git a/netbox/users/views.py b/netbox/users/views.py index d88217f40..15f4bd55f 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -5,6 +5,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse +from django.utils.decorators import method_decorator from django.utils.http import is_safe_url from django.views.generic import View @@ -19,9 +20,17 @@ from .models import Token # Login/logout # -def login(request): +class LoginView(View): + template_name = 'login.html' - if request.method == 'POST': + def get(self, request): + form = LoginForm(request) + + return render(request, self.template_name, { + 'form': form, + }) + + def post(self, request): form = LoginForm(request, data=request.POST) if form.is_valid(): @@ -36,37 +45,48 @@ def login(request): return HttpResponseRedirect(redirect_to) - else: - form = LoginForm() - - return render(request, 'login.html', { - 'form': form, - }) + return render(request, self.template_name, { + 'form': form, + }) -def logout(request): +class LogoutView(View): - auth_logout(request) - messages.info(request, u"You have logged out.") - return HttpResponseRedirect(reverse('home')) + def get(self, request): + auth_logout(request) + messages.info(request, u"You have logged out.") + + return HttpResponseRedirect(reverse('home')) # # User profiles # -@login_required() -def profile(request): +@method_decorator(login_required, name='dispatch') +class ProfileView(View): + template_name = 'users/profile.html' - return render(request, 'users/profile.html', { - 'active_tab': 'profile', - }) + def get(self, request): + + return render(request, self.template_name, { + 'active_tab': 'profile', + }) -@login_required() -def change_password(request): +@method_decorator(login_required, name='dispatch') +class ChangePasswordView(View): + template_name = 'users/change_password.html' - if request.method == 'POST': + def get(self, request): + form = PasswordChangeForm(user=request.user) + + return render(request, self.template_name, { + 'form': form, + 'active_tab': 'change_password', + }) + + def post(self, request): form = PasswordChangeForm(user=request.user, data=request.POST) if form.is_valid(): form.save() @@ -74,39 +94,51 @@ def change_password(request): messages.success(request, u"Your password has been changed successfully.") return redirect('user:profile') - else: - form = PasswordChangeForm(user=request.user) - - return render(request, 'users/change_password.html', { - 'form': form, - 'active_tab': 'change_password', - }) + return render(request, self.template_name, { + 'form': form, + 'active_tab': 'change_password', + }) -@login_required() -def userkey(request): +@method_decorator(login_required, name='dispatch') +class UserKeyView(View): + template_name = 'users/userkey.html' - try: - userkey = UserKey.objects.get(user=request.user) - except UserKey.DoesNotExist: - userkey = None + def get(self, request): + try: + userkey = UserKey.objects.get(user=request.user) + except UserKey.DoesNotExist: + userkey = None - return render(request, 'users/userkey.html', { - 'userkey': userkey, - 'active_tab': 'userkey', - }) + return render(request, self.template_name, { + 'userkey': userkey, + 'active_tab': 'userkey', + }) -@login_required() -def userkey_edit(request): +class UserKeyEditView(View): + template_name = 'users/userkey_edit.html' - try: - userkey = UserKey.objects.get(user=request.user) - except UserKey.DoesNotExist: - userkey = UserKey(user=request.user) + @method_decorator(login_required) + def dispatch(self, request, *args, **kwargs): + try: + self.userkey = UserKey.objects.get(user=request.user) + except UserKey.DoesNotExist: + self.userkey = UserKey(user=request.user) - if request.method == 'POST': - form = UserKeyForm(data=request.POST, instance=userkey) + return super(UserKeyEditView, self).dispatch(request, *args, **kwargs) + + def get(self, request): + form = UserKeyForm(instance=self.userkey) + + return render(request, self.template_name, { + 'userkey': self.userkey, + 'form': form, + 'active_tab': 'userkey', + }) + + def post(self, request): + form = UserKeyForm(data=request.POST, instance=self.userkey) if form.is_valid(): uk = form.save(commit=False) uk.user = request.user @@ -114,16 +146,14 @@ def userkey_edit(request): messages.success(request, u"Your user key has been saved.") return redirect('user:userkey') - else: - form = UserKeyForm(instance=userkey) - - return render(request, 'users/userkey_edit.html', { - 'userkey': userkey, - 'form': form, - 'active_tab': 'userkey', - }) + return render(request, self.template_name, { + 'userkey': self.userkey, + 'form': form, + 'active_tab': 'userkey', + }) +@method_decorator(login_required, name='dispatch') class SessionKeyDeleteView(LoginRequiredMixin, View): def get(self, request): @@ -160,13 +190,16 @@ class SessionKeyDeleteView(LoginRequiredMixin, View): }) -@login_required() -def recent_activity(request): +@method_decorator(login_required, name='dispatch') +class RecentActivityView(View): + template_name = 'users/recent_activity.html' - return render(request, 'users/recent_activity.html', { - 'recent_activity': request.user.actions.all()[:50], - 'active_tab': 'recent_activity', - }) + def get(self, request): + + return render(request, self.template_name, { + 'recent_activity': request.user.actions.all()[:50], + 'active_tab': 'recent_activity', + }) # From 713c7cd8e39be801bbc251cdb26ffac78ea2b678 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 19 May 2017 16:03:04 -0400 Subject: [PATCH 07/20] Cleaned up 500 error template --- netbox/templates/500.html | 45 +++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/netbox/templates/500.html b/netbox/templates/500.html index ba2ebf94f..575694b13 100644 --- a/netbox/templates/500.html +++ b/netbox/templates/500.html @@ -1,37 +1,40 @@ +{% load static from staticfiles %} Server Error - - + + -
-
-
-
- - - Server Error - -
-
-

There was a problem with your request. This error has been logged and administrative staff have - been notified. Please return to the home page and try again.

-

If you are responsible for this installation, please consider - filing a bug report. Additional - information is provided below:

-
{{ exception }}
+
+
+
+
+
+ + + Server Error + +
+
+

There was a problem with your request. This error has been logged and administrative staff have + been notified. Please return to the home page and try again.

+

If you are responsible for this installation, please consider + filing a bug report. Additional + information is provided below:

+
{{ exception }}
{{ error }}
-
-
+
From 3fa63b774ea5ca3da67d78460b41c5b467e3cf5d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 19 May 2017 16:03:51 -0400 Subject: [PATCH 08/20] Converted home view to a CBV --- netbox/netbox/urls.py | 4 +-- netbox/netbox/views.py | 66 ++++++++++++++++++++++-------------------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index a1e7eb351..6d946d90e 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -5,7 +5,7 @@ from django.conf.urls import include, url from django.contrib import admin from django.views.static import serve -from netbox.views import APIRootView, home, handle_500, SearchView, trigger_500 +from netbox.views import APIRootView, handle_500, HomeView, SearchView, trigger_500 from users.views import LoginView, LogoutView @@ -15,7 +15,7 @@ swagger_view = get_swagger_view(title='NetBox API') _patterns = [ # Base views - url(r'^$', home, name='home'), + url(r'^$', HomeView.as_view(), name='home'), url(r'^search/$', SearchView.as_view(), name='search'), # Login/logout diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index f602a46f4..793f3c9a2 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -115,43 +115,46 @@ SEARCH_TYPES = OrderedDict(( )) -def home(request): +class HomeView(View): + template_name = 'home.html' - stats = { + def get(self, request): - # Organization - 'site_count': Site.objects.count(), - 'tenant_count': Tenant.objects.count(), + stats = { - # DCIM - 'rack_count': Rack.objects.count(), - 'device_count': Device.objects.count(), - 'interface_connections_count': InterfaceConnection.objects.count(), - 'console_connections_count': ConsolePort.objects.filter(cs_port__isnull=False).count(), - 'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(), + # Organization + 'site_count': Site.objects.count(), + 'tenant_count': Tenant.objects.count(), - # IPAM - 'vrf_count': VRF.objects.count(), - 'aggregate_count': Aggregate.objects.count(), - 'prefix_count': Prefix.objects.count(), - 'ipaddress_count': IPAddress.objects.count(), - 'vlan_count': VLAN.objects.count(), + # DCIM + 'rack_count': Rack.objects.count(), + 'device_count': Device.objects.count(), + 'interface_connections_count': InterfaceConnection.objects.count(), + 'console_connections_count': ConsolePort.objects.filter(cs_port__isnull=False).count(), + 'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(), - # Circuits - 'provider_count': Provider.objects.count(), - 'circuit_count': Circuit.objects.count(), + # IPAM + 'vrf_count': VRF.objects.count(), + 'aggregate_count': Aggregate.objects.count(), + 'prefix_count': Prefix.objects.count(), + 'ipaddress_count': IPAddress.objects.count(), + 'vlan_count': VLAN.objects.count(), - # Secrets - 'secret_count': Secret.objects.count(), + # Circuits + 'provider_count': Provider.objects.count(), + 'circuit_count': Circuit.objects.count(), - } + # Secrets + 'secret_count': Secret.objects.count(), - return render(request, 'home.html', { - 'search_form': SearchForm(), - 'stats': stats, - 'topology_maps': TopologyMap.objects.filter(site__isnull=True), - 'recent_activity': UserAction.objects.select_related('user')[:50] - }) + } + + return render(request, self.template_name, { + 'search_form': SearchForm(), + 'stats': stats, + 'topology_maps': TopologyMap.objects.filter(site__isnull=True), + 'recent_activity': UserAction.objects.select_related('user')[:50] + }) class SearchView(View): @@ -235,5 +238,6 @@ def trigger_500(request): """ Hot-wired method of triggering a server error to test reporting """ - raise Exception("Congratulations, you've triggered an exception! Go tell all your friends what an exceptional " - "person you are.") + raise Exception( + "Congratulations, you've triggered an exception! Go tell all your friends what an exceptional person you are." + ) From 77f28e3441ff98c68083ecbd3ebdf5d21049bec0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 23 May 2017 22:12:17 -0400 Subject: [PATCH 09/20] Fixes #1214: Add status to list of required fields on child device import form --- netbox/templates/dcim/device_import_child.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/netbox/templates/dcim/device_import_child.html b/netbox/templates/dcim/device_import_child.html index 3fea2572a..668a9c810 100644 --- a/netbox/templates/dcim/device_import_child.html +++ b/netbox/templates/dcim/device_import_child.html @@ -69,6 +69,11 @@ Unique alphanumeric tag (optional) ABC123456 + + Status + Current status + Active + Parent device Parent device @@ -82,7 +87,7 @@

Example

-
Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,ABC123456,Server101,Slot4
+
Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,ABC123456,Active,Server101,Slot4
{% endblock %} From d5587de316179d382d1a2e4d80b8d3c47c27bcf6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 23 May 2017 22:15:13 -0400 Subject: [PATCH 10/20] Fixes #1213: Corrected table header ordering links --- netbox/templates/inc/table.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/inc/table.html b/netbox/templates/inc/table.html index e8d84fbeb..0c046ff2f 100644 --- a/netbox/templates/inc/table.html +++ b/netbox/templates/inc/table.html @@ -6,7 +6,7 @@ {% for column in table.columns %} {% if column.orderable %} - {{ column.header }} + {{ column.header }} {% else %} {{ column.header }} {% endif %} From 6719578f1433efb994f5274b5a8da7beae387070 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 23 May 2017 22:23:50 -0400 Subject: [PATCH 11/20] Fixes #1212: Allow assigning new VLANs to global VLAN groups --- netbox/utilities/forms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index dcf43d090..ba97a6507 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -447,6 +447,8 @@ class ChainedFieldsMixin(forms.BaseForm): filters_dict[db_field] = self.data[parent_field] elif self.initial.get(parent_field): filters_dict[db_field] = self.initial[parent_field] + elif self.fields[parent_field].widget.attrs.get('nullable'): + filters_dict[db_field] = None if filters_dict: field.queryset = field.queryset.filter(**filters_dict) From 1c489e57ccebe8ac0205ff9145ed9facece90f54 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 23 May 2017 22:36:40 -0400 Subject: [PATCH 12/20] Added a warning to note "untracked migrations" warnings during an upgrade --- docs/installation/upgrading.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index 193d7e74a..46b2252f7 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -58,6 +58,14 @@ This script: * Applies any database migrations that were included in the release * Collects all static files to be served by the HTTP service +!!! note + It's possible that the upgrade script will display a notice warning of unreflected database migrations: + + Your models have changes that are not yet reflected in a migration, and so won't be applied. + Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them. + + This may occur due to semantic differences in environment, and can be safely ignored. Never attempt to create new migrations unless you are inentionally modifying the database schema. + # Restart the WSGI Service Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`: From 68b6c7d886ee091674e4536626aa7879ac6431cc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 24 May 2017 09:40:24 -0400 Subject: [PATCH 13/20] Fixes #1210: Fix TemplateDoesNotExist errors on browsable API views --- netbox/netbox/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 348659e71..79fafecf5 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -112,6 +112,7 @@ INSTALLED_APPS = ( 'django.contrib.humanize', 'corsheaders', 'debug_toolbar', + 'django_filters', 'django_tables2', 'mptt', 'rest_framework', From 9aad8a77748a862dc2a78efdb1e7bc9700fe651c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 24 May 2017 10:34:01 -0400 Subject: [PATCH 14/20] Fixes #1219: Fix image attachment URLs when BASE_PATH is set --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 79fafecf5..61f213be6 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -181,8 +181,8 @@ STATICFILES_DIRS = ( ) # Media -MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_URL = '/{}media/'.format(BASE_PATH) # Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.) DATA_UPLOAD_MAX_NUMBER_FIELDS = None From f21c6bca0014d6b04f320efb9c9dd820f2d81b8d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 24 May 2017 11:33:11 -0400 Subject: [PATCH 15/20] Import unicode_literals --- netbox/circuits/api/serializers.py | 2 + netbox/circuits/api/urls.py | 2 + netbox/circuits/api/views.py | 4 +- netbox/circuits/apps.py | 2 + netbox/circuits/filters.py | 2 + netbox/circuits/forms.py | 2 + netbox/circuits/models.py | 6 ++- netbox/circuits/signals.py | 2 + netbox/circuits/tables.py | 3 +- netbox/circuits/tests/test_api.py | 2 + netbox/circuits/urls.py | 2 + netbox/circuits/views.py | 3 +- netbox/dcim/api/exceptions.py | 2 + netbox/dcim/api/serializers.py | 2 + netbox/dcim/api/urls.py | 2 + netbox/dcim/api/views.py | 2 + netbox/dcim/apps.py | 2 + netbox/dcim/fields.py | 2 + netbox/dcim/filters.py | 2 + netbox/dcim/formfields.py | 2 + netbox/dcim/forms.py | 9 ++-- netbox/dcim/models.py | 26 +++++------ netbox/dcim/tables.py | 3 +- netbox/dcim/tests/test_api.py | 2 + netbox/dcim/tests/test_forms.py | 3 ++ netbox/dcim/tests/test_models.py | 3 ++ netbox/dcim/urls.py | 5 ++- netbox/dcim/views.py | 44 +++++++++---------- netbox/extras/admin.py | 2 + netbox/extras/api/customfields.py | 12 ++--- netbox/extras/api/serializers.py | 4 +- netbox/extras/api/urls.py | 2 + netbox/extras/api/views.py | 2 + netbox/extras/filters.py | 2 + netbox/extras/forms.py | 3 +- .../management/commands/run_inventory.py | 2 + netbox/extras/models.py | 11 ++--- netbox/extras/rpc.py | 6 ++- netbox/extras/tests/test_api.py | 2 + netbox/extras/tests/test_customfields.py | 2 +- netbox/extras/urls.py | 2 + netbox/extras/views.py | 2 + netbox/ipam/api/serializers.py | 2 + netbox/ipam/api/urls.py | 2 + netbox/ipam/api/views.py | 2 + netbox/ipam/apps.py | 2 + netbox/ipam/fields.py | 2 + netbox/ipam/filters.py | 3 +- netbox/ipam/formfields.py | 2 + netbox/ipam/forms.py | 9 ++-- netbox/ipam/lookups.py | 2 + netbox/ipam/models.py | 9 ++-- netbox/ipam/tables.py | 3 +- netbox/ipam/tests/test_api.py | 3 +- netbox/ipam/tests/test_models.py | 4 +- netbox/ipam/urls.py | 2 + netbox/ipam/views.py | 3 +- netbox/netbox/forms.py | 2 + netbox/netbox/urls.py | 2 + netbox/netbox/views.py | 5 ++- netbox/netbox/wsgi.py | 1 + netbox/secrets/admin.py | 6 ++- netbox/secrets/api/serializers.py | 2 + netbox/secrets/api/urls.py | 2 + netbox/secrets/api/views.py | 7 +-- netbox/secrets/decorators.py | 6 ++- netbox/secrets/exceptions.py | 3 ++ netbox/secrets/filters.py | 2 + netbox/secrets/forms.py | 3 +- netbox/secrets/hashers.py | 2 + netbox/secrets/models.py | 7 +-- netbox/secrets/tables.py | 8 ++-- netbox/secrets/templatetags/secret_helpers.py | 2 + netbox/secrets/tests/test_api.py | 2 + netbox/secrets/tests/test_models.py | 2 + netbox/secrets/urls.py | 2 + netbox/secrets/views.py | 10 ++--- netbox/tenancy/api/serializers.py | 2 + netbox/tenancy/api/urls.py | 2 + netbox/tenancy/api/views.py | 5 ++- netbox/tenancy/apps.py | 2 + netbox/tenancy/filters.py | 2 + netbox/tenancy/forms.py | 2 + netbox/tenancy/models.py | 2 + netbox/tenancy/tables.py | 8 ++-- netbox/tenancy/tests/test_api.py | 2 + netbox/tenancy/urls.py | 2 + netbox/tenancy/views.py | 3 +- netbox/users/admin.py | 2 + netbox/users/api/serializers.py | 2 + netbox/users/forms.py | 2 + netbox/users/models.py | 3 +- netbox/users/urls.py | 2 + netbox/users/views.py | 10 +++-- netbox/utilities/api.py | 2 + netbox/utilities/context_processors.py | 2 + netbox/utilities/error_handlers.py | 10 +++-- netbox/utilities/fields.py | 2 + netbox/utilities/filters.py | 2 + netbox/utilities/forms.py | 3 +- netbox/utilities/managers.py | 2 + netbox/utilities/middleware.py | 2 + netbox/utilities/models.py | 2 + netbox/utilities/paginator.py | 2 + netbox/utilities/sql.py | 2 + netbox/utilities/tables.py | 2 + netbox/utilities/tests.py | 2 + netbox/utilities/utils.py | 13 +++--- netbox/utilities/views.py | 27 ++++++------ 109 files changed, 320 insertions(+), 131 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index b36d22105..f2e6d0d00 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import serializers from circuits.models import Provider, Circuit, CircuitTermination, CircuitType diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index dfe0624e8..25df44bfd 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import routers from . import views diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 582de3ab1..d14080531 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -1,9 +1,11 @@ -from django.shortcuts import get_object_or_404 +from __future__ import unicode_literals from rest_framework.decorators import detail_route from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet +from django.shortcuts import get_object_or_404 + from circuits import filters from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from extras.models import Graph, GRAPH_TYPE_PROVIDER diff --git a/netbox/circuits/apps.py b/netbox/circuits/apps.py index bc0b7d87d..613c347f2 100644 --- a/netbox/circuits/apps.py +++ b/netbox/circuits/apps.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.apps import AppConfig diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index e4a11faf0..6e9e1f443 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import django_filters from django.db.models import Q diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index f81abff04..eb38b8102 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django import forms from django.db.models import Count diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 04d9b3e13..7da5c4f73 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.urls import reverse @@ -110,7 +112,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): unique_together = ['provider', 'cid'] def __str__(self): - return u'{} {}'.format(self.provider, self.cid) + return '{} {}'.format(self.provider, self.cid) def get_absolute_url(self): return reverse('circuits:circuit', args=[self.pk]) @@ -166,7 +168,7 @@ class CircuitTermination(models.Model): unique_together = ['circuit', 'term_side'] def __str__(self): - return u'{} (Side {})'.format(self.circuit, self.get_term_side_display()) + return '{} (Side {})'.format(self.circuit, self.get_term_side_display()) def get_peer_termination(self): peer_side = 'Z' if self.term_side == 'A' else 'A' diff --git a/netbox/circuits/signals.py b/netbox/circuits/signals.py index bdfe8c0b6..40a1e1031 100644 --- a/netbox/circuits/signals.py +++ b/netbox/circuits/signals.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.db.models.signals import post_delete, post_save from django.dispatch import receiver from django.utils import timezone diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index 3cda30ccc..d09c5a7b2 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -1,8 +1,9 @@ +from __future__ import unicode_literals + import django_tables2 as tables from django_tables2.utils import Accessor from utilities.tables import BaseTable, SearchTable, ToggleColumn - from .models import Circuit, CircuitType, Provider diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index 7bd3d8040..fc39b72de 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import status from rest_framework.test import APITestCase diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index be2791382..12a7dc298 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf.urls import url from . import views diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index d1f192030..eed612a33 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import PermissionRequiredMixin @@ -12,7 +14,6 @@ from utilities.forms import ConfirmationForm from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) - from . import filters, forms, tables from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z diff --git a/netbox/dcim/api/exceptions.py b/netbox/dcim/api/exceptions.py index 05ad86b5b..8804da436 100644 --- a/netbox/dcim/api/exceptions.py +++ b/netbox/dcim/api/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework.exceptions import APIException diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 4c9c10804..8ca6cab35 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index db537e0b7..6f16310e5 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import routers from . import views diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 82fc5d6ab..116aaa77c 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework.decorators import detail_route from rest_framework.mixins import ListModelMixin from rest_framework.permissions import IsAuthenticated diff --git a/netbox/dcim/apps.py b/netbox/dcim/apps.py index fdfcc1f57..fb1f4ee39 100644 --- a/netbox/dcim/apps.py +++ b/netbox/dcim/apps.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.apps import AppConfig diff --git a/netbox/dcim/fields.py b/netbox/dcim/fields.py index 6b45f6e65..22e0be581 100644 --- a/netbox/dcim/fields.py +++ b/netbox/dcim/fields.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from netaddr import EUI, mac_unix_expanded from django.core.exceptions import ValidationError diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 39838a265..93a325d98 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import django_filters from netaddr.core import AddrFormatError diff --git a/netbox/dcim/formfields.py b/netbox/dcim/formfields.py index 4e568c2e6..83054c088 100644 --- a/netbox/dcim/formfields.py +++ b/netbox/dcim/formfields.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from netaddr import EUI, AddrFormatError from django import forms diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index c110f1d47..03fddf21d 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from mptt.forms import TreeNodeChoiceField import re @@ -16,7 +18,6 @@ from utilities.forms import ( FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField, ) - from .formfields import MACAddressFormField from .models import ( DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, @@ -610,10 +611,10 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): for family in [4, 6]: ip_choices = [] interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance) - ip_choices += [(ip.id, u'{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips] + ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips] nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\ .select_related('nat_inside__interface') - ip_choices += [(ip.id, u'{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips] + ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips] self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device @@ -804,7 +805,7 @@ def device_status_choices(): status_counts = {} for status in Device.objects.values('status').annotate(count=Count('status')).order_by('status'): status_counts[status['status']] = status['count'] - return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in STATUS_CHOICES] + return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in STATUS_CHOICES] class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 59d7a0ef2..043df10dc 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from collections import OrderedDict from itertools import count, groupby @@ -23,7 +24,6 @@ from utilities.fields import ColorField, NullableCharField from utilities.managers import NaturalOrderByManager from utilities.models import CreatedUpdatedModel from utilities.utils import csv_format - from .fields import ASNField, MACAddressField @@ -346,7 +346,7 @@ class RackGroup(models.Model): ] def __str__(self): - return u'{} - {}'.format(self.site.name, self.name) + return '{} - {}'.format(self.site.name, self.name) def get_absolute_url(self): return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk) @@ -466,10 +466,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): @property def display_name(self): if self.facility_id: - return u"{} ({})".format(self.name, self.facility_id) + return "{} ({})".format(self.name, self.facility_id) elif self.name: return self.name - return u"" + return "" def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False): """ @@ -569,7 +569,7 @@ class RackReservation(models.Model): ordering = ['created'] def __str__(self): - return u"Reservation for rack {}".format(self.rack) + return "Reservation for rack {}".format(self.rack) def clean(self): @@ -579,7 +579,7 @@ class RackReservation(models.Model): invalid_units = [u for u in self.units if u not in self.rack.units] if invalid_units: raise ValidationError({ - 'units': u"Invalid unit(s) for {}U rack: {}".format( + 'units': "Invalid unit(s) for {}U rack: {}".format( self.rack.u_height, ', '.join([str(u) for u in invalid_units]), ), @@ -733,7 +733,7 @@ class DeviceType(models.Model, CustomFieldModel): @property def full_name(self): - return u'{} {}'.format(self.manufacturer.name, self.model) + return '{} {}'.format(self.manufacturer.name, self.model) @property def is_parent_device(self): @@ -1106,8 +1106,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel): if self.name: return self.name elif hasattr(self, 'device_type'): - return u"{}".format(self.device_type) - return u"" + return "{}".format(self.device_type) + return "" @property def identifier(self): @@ -1320,7 +1320,7 @@ class Interface(models.Model): # An interface's LAG must belong to the same device if self.lag and self.lag.device != self.device: raise ValidationError({ - 'lag': u"The selected LAG interface ({}) belongs to a different device ({}).".format( + 'lag': "The selected LAG interface ({}) belongs to a different device ({}).".format( self.lag.name, self.lag.device.name ) }) @@ -1328,14 +1328,14 @@ class Interface(models.Model): # A virtual interface cannot have a parent LAG if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None: raise ValidationError({ - 'lag': u"{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display()) + 'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display()) }) # Only a LAG can have LAG members if self.form_factor != IFACE_FF_LAG and self.member_interfaces.exists(): raise ValidationError({ 'form_factor': "Cannot change interface form factor; it has LAG members ({}).".format( - u", ".join([iface.name for iface in self.member_interfaces.all()]) + ", ".join([iface.name for iface in self.member_interfaces.all()]) ) }) @@ -1428,7 +1428,7 @@ class DeviceBay(models.Model): unique_together = ['device', 'name'] def __str__(self): - return u'{} - {}'.format(self.device.name, self.name) + return '{} - {}'.format(self.device.name, self.name) def clean(self): diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index d15e274c7..be80233e0 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -1,8 +1,9 @@ +from __future__ import unicode_literals + import django_tables2 as tables from django_tables2.utils import Accessor from utilities.tables import BaseTable, SearchTable, ToggleColumn - from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 2f3b38449..9fe191cc7 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import status from rest_framework.test import APITestCase diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index f859fe5e1..acf71411e 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -1,4 +1,7 @@ +from __future__ import unicode_literals + from django.test import TestCase + from dcim.forms import * from dcim.models import * diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index d1b721cb0..340c58092 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -1,4 +1,7 @@ +from __future__ import unicode_literals + from django.test import TestCase + from dcim.models import * diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index bfe74dec6..775daeabf 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -1,9 +1,10 @@ +from __future__ import unicode_literals + from django.conf.urls import url +from extras.views import ImageAttachmentEditView from ipam.views import ServiceEditView from secrets.views import secret_add - -from extras.views import ImageAttachmentEditView from .models import Device, Rack, Site from . import views diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 0e3028e49..322e88b72 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from copy import deepcopy import re from natsort import natsorted @@ -24,7 +25,6 @@ from utilities.paginator import EnhancedPaginator from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) - from . import filters, forms, tables from .models import ( CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, @@ -109,11 +109,11 @@ class ComponentCreateView(View): if field == 'name': field = 'name_pattern' for e in errors: - form.add_error(field, u'{}: {}'.format(name, ', '.join(e))) + form.add_error(field, '{}: {}'.format(name, ', '.join(e))) if not form.errors: self.model.objects.bulk_create(new_components) - messages.success(request, u"Added {} {} to {}.".format( + messages.success(request, "Added {} {} to {}.".format( len(new_components), self.model._meta.verbose_name_plural, parent )) if '_addanother' in request.POST: @@ -930,7 +930,7 @@ def consoleport_connect(request, pk): form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport) if form.is_valid(): consoleport = form.save() - msg = u'Connected {} {} to {} {}'.format( + msg = 'Connected {} {} to {} {}'.format( consoleport.device.get_absolute_url(), escape(consoleport.device), escape(consoleport.name), @@ -964,7 +964,7 @@ def consoleport_disconnect(request, pk): if not consoleport.cs_port: messages.warning( - request, u"Cannot disconnect console port {}: It is not connected to anything.".format(consoleport) + request, "Cannot disconnect console port {}: It is not connected to anything.".format(consoleport) ) return redirect('dcim:device', pk=consoleport.device.pk) @@ -975,7 +975,7 @@ def consoleport_disconnect(request, pk): consoleport.cs_port = None consoleport.connection_status = None consoleport.save() - msg = u'Disconnected {} {} from {} {}'.format( + msg = 'Disconnected {} {} from {} {}'.format( consoleport.device.get_absolute_url(), escape(consoleport.device), escape(consoleport.name), @@ -1047,7 +1047,7 @@ def consoleserverport_connect(request, pk): consoleport.cs_port = consoleserverport consoleport.connection_status = form.cleaned_data['connection_status'] consoleport.save() - msg = u'Connected {} {} to {} {}'.format( + msg = 'Connected {} {} to {} {}'.format( consoleport.device.get_absolute_url(), escape(consoleport.device), escape(consoleport.name), @@ -1081,7 +1081,7 @@ def consoleserverport_disconnect(request, pk): if not hasattr(consoleserverport, 'connected_console'): messages.warning( - request, u"Cannot disconnect console server port {}: Nothing is connected to it.".format(consoleserverport) + request, "Cannot disconnect console server port {}: Nothing is connected to it.".format(consoleserverport) ) return redirect('dcim:device', pk=consoleserverport.device.pk) @@ -1092,7 +1092,7 @@ def consoleserverport_disconnect(request, pk): consoleport.cs_port = None consoleport.connection_status = None consoleport.save() - msg = u'Disconnected {} {} from {} {}'.format( + msg = 'Disconnected {} {} from {} {}'.format( consoleport.device.get_absolute_url(), escape(consoleport.device), escape(consoleport.name), @@ -1153,7 +1153,7 @@ def powerport_connect(request, pk): form = forms.PowerPortConnectionForm(request.POST, instance=powerport) if form.is_valid(): powerport = form.save() - msg = u'Connected {} {} to {} {}'.format( + msg = 'Connected {} {} to {} {}'.format( powerport.device.get_absolute_url(), escape(powerport.device), escape(powerport.name), @@ -1187,7 +1187,7 @@ def powerport_disconnect(request, pk): if not powerport.power_outlet: messages.warning( - request, u"Cannot disconnect power port {}: It is not connected to an outlet.".format(powerport) + request, "Cannot disconnect power port {}: It is not connected to an outlet.".format(powerport) ) return redirect('dcim:device', pk=powerport.device.pk) @@ -1198,7 +1198,7 @@ def powerport_disconnect(request, pk): powerport.power_outlet = None powerport.connection_status = None powerport.save() - msg = u'Disconnected {} {} from {} {}'.format( + msg = 'Disconnected {} {} from {} {}'.format( powerport.device.get_absolute_url(), escape(powerport.device), escape(powerport.name), @@ -1270,7 +1270,7 @@ def poweroutlet_connect(request, pk): powerport.power_outlet = poweroutlet powerport.connection_status = form.cleaned_data['connection_status'] powerport.save() - msg = u'Connected {} {} to {} {}'.format( + msg = 'Connected {} {} to {} {}'.format( powerport.device.get_absolute_url(), escape(powerport.device), escape(powerport.name), @@ -1304,7 +1304,7 @@ def poweroutlet_disconnect(request, pk): if not hasattr(poweroutlet, 'connected_port'): messages.warning( - request, u"Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet) + request, "Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet) ) return redirect('dcim:device', pk=poweroutlet.device.pk) @@ -1315,7 +1315,7 @@ def poweroutlet_disconnect(request, pk): powerport.power_outlet = None powerport.connection_status = None powerport.save() - msg = u'Disconnected {} {} from {} {}'.format( + msg = 'Disconnected {} {} from {} {}'.format( powerport.device.get_absolute_url(), escape(powerport.device), escape(powerport.name), @@ -1429,7 +1429,7 @@ def devicebay_populate(request, pk): device_bay.save() if not form.errors: - messages.success(request, u"Added {} to {}.".format(device_bay.installed_device, device_bay)) + messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay)) return redirect('dcim:device', pk=device_bay.device.pk) else: @@ -1453,7 +1453,7 @@ def devicebay_depopulate(request, pk): removed_device = device_bay.installed_device device_bay.installed_device = None device_bay.save() - messages.success(request, u"{} has been removed from {}.".format(removed_device, device_bay)) + messages.success(request, "{} has been removed from {}.".format(removed_device, device_bay)) return redirect('dcim:device', pk=device_bay.device.pk) else: @@ -1516,11 +1516,11 @@ class DeviceBulkAddComponentView(View): else: for field, errors in component_form.errors.as_data().items(): for e in errors: - form.add_error(field, u'{} {}: {}'.format(device, name, ', '.join(e))) + form.add_error(field, '{} {}: {}'.format(device, name, ', '.join(e))) if not form.errors: self.model.objects.bulk_create(new_components) - messages.success(request, u"Added {} {} to {} devices.".format( + messages.success(request, "Added {} {} to {} devices.".format( len(new_components), self.model._meta.verbose_name_plural, len(form.cleaned_data['pk']) )) return redirect('dcim:device_list') @@ -1530,7 +1530,7 @@ class DeviceBulkAddComponentView(View): selected_devices = Device.objects.filter(pk__in=pk_list) if not selected_devices: - messages.warning(request, u"No devices were selected.") + messages.warning(request, "No devices were selected.") return redirect('dcim:device_list') return render(request, 'dcim/device_bulk_add_component.html', { @@ -1592,7 +1592,7 @@ def interfaceconnection_add(request, pk): if form.is_valid(): interfaceconnection = form.save() - msg = u'Connected {} {} to {} {}'.format( + msg = 'Connected {} {} to {} {}'.format( interfaceconnection.interface_a.device.get_absolute_url(), escape(interfaceconnection.interface_a.device), escape(interfaceconnection.interface_a.name), @@ -1640,7 +1640,7 @@ def interfaceconnection_delete(request, pk): form = forms.InterfaceConnectionDeletionForm(request.POST) if form.is_valid(): interfaceconnection.delete() - msg = u'Disconnected {} {} from {} {}'.format( + msg = 'Disconnected {} {} from {} {}'.format( interfaceconnection.interface_a.device.get_absolute_url(), escape(interfaceconnection.interface_a.device), escape(interfaceconnection.interface_a.name), diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 2a06b3f2f..9d396dd3d 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django import forms from django.contrib import admin from django.utils.safestring import mark_safe diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index dafed750b..5bd221893 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -1,9 +1,11 @@ -from django.contrib.contenttypes.models import ContentType -from django.db import transaction +from __future__ import unicode_literals from rest_framework import serializers from rest_framework.exceptions import ValidationError +from django.contrib.contenttypes.models import ContentType +from django.db import transaction + from extras.models import CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue @@ -25,14 +27,14 @@ class CustomFieldsSerializer(serializers.BaseSerializer): # Validate custom field name if field_name not in custom_fields: - raise ValidationError(u"Invalid custom field for {} objects: {}".format(content_type, field_name)) + raise ValidationError("Invalid custom field for {} objects: {}".format(content_type, field_name)) # Validate selected choice cf = custom_fields[field_name] if cf.type == CF_TYPE_SELECT: valid_choices = [c.pk for c in cf.choices.all()] if value not in valid_choices: - raise ValidationError(u"Invalid choice ({}) for field {}".format(value, field_name)) + raise ValidationError("Invalid choice ({}) for field {}".format(value, field_name)) # Check for missing required fields missing_fields = [] @@ -40,7 +42,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer): if field.required and field_name not in data: missing_fields.append(field_name) if missing_fields: - raise ValidationError(u"Missing required fields: {}".format(u", ".join(missing_fields))) + raise ValidationError("Missing required fields: {}".format(u", ".join(missing_fields))) return data diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 08da93aa0..c8b3ff6f7 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -1,7 +1,9 @@ -from rest_framework import serializers +from __future__ import unicode_literals from django.core.exceptions import ObjectDoesNotExist +from rest_framework import serializers + from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer from dcim.models import Device, Rack, Site from extras.models import ( diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index 0f4bc2874..c5268318c 100644 --- a/netbox/extras/api/urls.py +++ b/netbox/extras/api/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import routers from . import views diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index d8ef9090e..37112f2c6 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework.decorators import detail_route from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index 9d9dc5f87..e44fb86e9 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import django_filters from django.contrib.auth.models import User diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index d85697c8d..d7a06fa5f 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from collections import OrderedDict from django import forms @@ -104,7 +105,7 @@ class CustomFieldForm(forms.ModelForm): obj_id=self.instance.pk) except CustomFieldValue.DoesNotExist: # Skip this field if none exists already and its value is empty - if self.cleaned_data[field_name] in [None, u'']: + if self.cleaned_data[field_name] in [None, '']: continue cfv = CustomFieldValue( field=self.fields[field_name].model, diff --git a/netbox/extras/management/commands/run_inventory.py b/netbox/extras/management/commands/run_inventory.py index c8008ed18..1e52b5c8f 100644 --- a/netbox/extras/management/commands/run_inventory.py +++ b/netbox/extras/management/commands/run_inventory.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from getpass import getpass from ncclient.transport.errors import AuthenticationError from paramiko import AuthenticationException diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 66d44d8a5..ea92fae0c 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from collections import OrderedDict from datetime import date import graphviz @@ -175,7 +176,7 @@ class CustomFieldValue(models.Model): unique_together = ['field', 'obj_type', 'obj_id'] def __str__(self): - return u'{} {}'.format(self.obj, self.field) + return '{} {}'.format(self.obj, self.field) @property def value(self): @@ -269,7 +270,7 @@ class ExportTemplate(models.Model): ] def __str__(self): - return u'{}: {}'.format(self.content_type, self.name) + return '{}: {}'.format(self.content_type, self.name) def to_response(self, context_dict, filename): """ @@ -387,7 +388,7 @@ def image_upload(instance, filename): elif instance.name: filename = instance.name - return u'{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename) + return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename) @python_2_unicode_compatible @@ -503,8 +504,8 @@ class UserAction(models.Model): def __str__(self): if self.message: - return u'{} {}'.format(self.user, self.message) - return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type) + return '{} {}'.format(self.user, self.message) + return '{} {} {}'.format(self.user, self.get_action_display(), self.content_type) def icon(self): if self.action in [ACTION_CREATE, ACTION_BULK_CREATE, ACTION_IMPORT]: diff --git a/netbox/extras/rpc.py b/netbox/extras/rpc.py index 208ec20dd..613bdb743 100644 --- a/netbox/extras/rpc.py +++ b/netbox/extras/rpc.py @@ -1,8 +1,10 @@ +from __future__ import unicode_literals +import re +import time + from ncclient import manager import paramiko -import re import xmltodict -import time CONNECT_TIMEOUT = 5 # seconds diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 4c80ddee8..eddc6d71f 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import status from rest_framework.test import APITestCase diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 9e475fde8..5bbb407ce 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from datetime import date from rest_framework import status @@ -9,7 +10,6 @@ from django.test import TestCase from django.urls import reverse from dcim.models import Site - from extras.models import ( CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL, diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 635c9edb4..f980158e8 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf.urls import url from extras import views diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 97968c62b..6469ba76a 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib.auth.mixins import PermissionRequiredMixin from django.shortcuts import get_object_or_404 diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 7d9a5778c..096f1d232 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator diff --git a/netbox/ipam/api/urls.py b/netbox/ipam/api/urls.py index b3e559675..e6b1bb13d 100644 --- a/netbox/ipam/api/urls.py +++ b/netbox/ipam/api/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import routers from . import views diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 611931edf..87c1996a1 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework.viewsets import ModelViewSet from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF diff --git a/netbox/ipam/apps.py b/netbox/ipam/apps.py index fd4af74b0..c944d1b2c 100644 --- a/netbox/ipam/apps.py +++ b/netbox/ipam/apps.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.apps import AppConfig diff --git a/netbox/ipam/fields.py b/netbox/ipam/fields.py index c44385b6d..a20a5dce2 100644 --- a/netbox/ipam/fields.py +++ b/netbox/ipam/fields.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from netaddr import IPNetwork from django.core.exceptions import ValidationError diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 3229ad2b8..54146e91a 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import django_filters from netaddr import IPNetwork from netaddr.core import AddrFormatError @@ -8,7 +10,6 @@ from dcim.models import Site, Device, Interface from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter - from .models import ( Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF, diff --git a/netbox/ipam/formfields.py b/netbox/ipam/formfields.py index 914310be9..8d30e11e5 100644 --- a/netbox/ipam/formfields.py +++ b/netbox/ipam/formfields.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from netaddr import IPNetwork, AddrFormatError from django import forms diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index e45543479..7f62015af 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django import forms from django.core.exceptions import ValidationError from django.db.models import Count @@ -10,7 +12,6 @@ from utilities.forms import ( APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedModelChoiceField, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice, ) - from .models import ( Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF, @@ -270,7 +271,7 @@ def prefix_status_choices(): status_counts = {} for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'): status_counts[status['status']] = status['count'] - return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES] + return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES] class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): @@ -567,7 +568,7 @@ def ipaddress_status_choices(): status_counts = {} for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'): status_counts[status['status']] = status['count'] - return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES] + return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES] class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): @@ -720,7 +721,7 @@ def vlan_status_choices(): status_counts = {} for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'): status_counts[status['status']] = status['count'] - return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES] + return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES] class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): diff --git a/netbox/ipam/lookups.py b/netbox/ipam/lookups.py index 05c69dfb2..ef5cf8327 100644 --- a/netbox/ipam/lookups.py +++ b/netbox/ipam/lookups.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.db.models import Lookup, Transform, IntegerField from django.db.models.lookups import BuiltinLookup diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 980b17913..01cdd406d 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from netaddr import IPNetwork, cidr_merge from django.conf import settings @@ -15,7 +17,6 @@ from tenancy.models import Tenant from utilities.models import CreatedUpdatedModel from utilities.sql import NullsFirstQuerySet from utilities.utils import csv_format - from .fields import IPNetworkField, IPAddressField @@ -499,7 +500,7 @@ class VLANGroup(models.Model): def __str__(self): if self.site is None: return self.name - return u'{} - {}'.format(self.site.name, self.name) + return '{} - {}'.format(self.site.name, self.name) def get_absolute_url(self): return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk) @@ -566,7 +567,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): @property def display_name(self): if self.vid and self.name: - return u"{} ({})".format(self.vid, self.name) + return "{} ({})".format(self.vid, self.name) return None def get_status_class(self): @@ -593,4 +594,4 @@ class Service(CreatedUpdatedModel): unique_together = ['device', 'protocol', 'port'] def __str__(self): - return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display()) + return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display()) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 562713f5b..64b7d62fa 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -1,8 +1,9 @@ +from __future__ import unicode_literals + import django_tables2 as tables from django_tables2.utils import Accessor from utilities.tables import BaseTable, SearchTable, ToggleColumn - from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index cd58f865b..0b6814b4a 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -1,5 +1,6 @@ -from netaddr import IPNetwork +from __future__ import unicode_literals +from netaddr import IPNetwork from rest_framework import status from rest_framework.test import APITestCase diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index 3385c643f..0f75cc795 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -1,9 +1,11 @@ +from __future__ import unicode_literals + import netaddr +from django.core.exceptions import ValidationError from django.test import TestCase, override_settings from ipam.models import IPAddress, Prefix, VRF -from django.core.exceptions import ValidationError class TestPrefix(TestCase): diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index 59b1d1fd3..d28bf8a13 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf.urls import url from . import views diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index b49db4250..a51f47b6e 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django_tables2 import RequestConfig import netaddr @@ -13,7 +15,6 @@ from utilities.paginator import EnhancedPaginator from utilities.views import ( BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) - from . import filters, forms, tables from .models import ( Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role, diff --git a/netbox/netbox/forms.py b/netbox/netbox/forms.py index 63af2e04b..85343ec77 100644 --- a/netbox/netbox/forms.py +++ b/netbox/netbox/forms.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django import forms from utilities.forms import BootstrapMixin diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 6d946d90e..ddddf27a2 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework_swagger.views import get_swagger_view from django.conf import settings diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 793f3c9a2..6f642063b 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from collections import OrderedDict import sys @@ -195,7 +196,7 @@ class SearchView(View): results.append({ 'name': queryset.model._meta.verbose_name_plural, 'table': table, - 'url': u'{}?q={}'.format(reverse(url), form.cleaned_data['q']) + 'url': '{}?q={}'.format(reverse(url), form.cleaned_data['q']) }) return render(request, 'search.html', { @@ -209,7 +210,7 @@ class APIRootView(APIView): exclude_from_schema = True def get_view_name(self): - return u"API Root" + return "API Root" def get(self, request, format=None): diff --git a/netbox/netbox/wsgi.py b/netbox/netbox/wsgi.py index 7fac23c61..6d13ffe9d 100644 --- a/netbox/netbox/wsgi.py +++ b/netbox/netbox/wsgi.py @@ -11,6 +11,7 @@ import os from django.core.wsgi import get_wsgi_application + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings") application = get_wsgi_application() diff --git a/netbox/secrets/admin.py b/netbox/secrets/admin.py index 58658a07e..3780dccd2 100644 --- a/netbox/secrets/admin.py +++ b/netbox/secrets/admin.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib import admin, messages from django.shortcuts import redirect, render @@ -34,7 +36,7 @@ class UserKeyAdmin(admin.ModelAdmin): try: my_userkey = UserKey.objects.get(user=request.user) except UserKey.DoesNotExist: - messages.error(request, u"You do not have an active User Key.") + messages.error(request, "You do not have an active User Key.") return redirect('admin:secrets_userkey_changelist') if 'activate' in request.POST: @@ -46,7 +48,7 @@ class UserKeyAdmin(admin.ModelAdmin): uk.activate(master_key) return redirect('admin:secrets_userkey_changelist') except ValueError: - messages.error(request, u"Invalid private key provided. Unable to retrieve master key.") + messages.error(request, "Invalid private key provided. Unable to retrieve master key.") else: form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)}) diff --git a/netbox/secrets/api/serializers.py b/netbox/secrets/api/serializers.py index 1dc52388a..3c7132d37 100644 --- a/netbox/secrets/api/serializers.py +++ b/netbox/secrets/api/serializers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator diff --git a/netbox/secrets/api/urls.py b/netbox/secrets/api/urls.py index 5f60a8a06..3b1e7d3d0 100644 --- a/netbox/secrets/api/urls.py +++ b/netbox/secrets/api/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import routers from . import views diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 0e6314e17..edc165aa0 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -1,13 +1,14 @@ +from __future__ import unicode_literals import base64 + from Crypto.PublicKey import RSA - -from django.http import HttpResponseBadRequest - from rest_framework.exceptions import ValidationError from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet, ViewSet +from django.http import HttpResponseBadRequest + from secrets import filters from secrets.exceptions import InvalidKey from secrets.models import Secret, SecretRole, SessionKey, UserKey diff --git a/netbox/secrets/decorators.py b/netbox/secrets/decorators.py index 683805124..0b9ebc16e 100644 --- a/netbox/secrets/decorators.py +++ b/netbox/secrets/decorators.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib import messages from django.shortcuts import redirect @@ -14,10 +16,10 @@ def userkey_required(): try: uk = UserKey.objects.get(user=request.user) except UserKey.DoesNotExist: - messages.warning(request, u"This operation requires an active user key, but you don't have one.") + messages.warning(request, "This operation requires an active user key, but you don't have one.") return redirect('user:userkey') if not uk.is_active(): - messages.warning(request, u"This operation is not available. Your user key has not been activated.") + messages.warning(request, "This operation is not available. Your user key has not been activated.") return redirect('user:userkey') return view(request, *args, **kwargs) return wrapped_view diff --git a/netbox/secrets/exceptions.py b/netbox/secrets/exceptions.py index c163e5907..f014d8a14 100644 --- a/netbox/secrets/exceptions.py +++ b/netbox/secrets/exceptions.py @@ -1,3 +1,6 @@ +from __future__ import unicode_literals + + class InvalidKey(Exception): """ Raised when a provided key is invalid. diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py index 14ebd1616..4bc7b56cd 100644 --- a/netbox/secrets/filters.py +++ b/netbox/secrets/filters.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import django_filters from django.db.models import Q diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 65e369376..b8e165804 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from Crypto.Cipher import PKCS1_OAEP from Crypto.PublicKey import RSA @@ -6,7 +8,6 @@ from django.db.models import Count from dcim.models import Device from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField - from .models import Secret, SecretRole, UserKey diff --git a/netbox/secrets/hashers.py b/netbox/secrets/hashers.py index fc5066fc6..49da1605d 100644 --- a/netbox/secrets/hashers.py +++ b/netbox/secrets/hashers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib.auth.hashers import PBKDF2PasswordHasher diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index e39f1832b..bf423fdf6 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -1,4 +1,6 @@ +from __future__ import unicode_literals import os + from Crypto.Cipher import AES, PKCS1_OAEP, XOR from Crypto.PublicKey import RSA @@ -12,7 +14,6 @@ from django.utils.encoding import force_bytes, python_2_unicode_compatible from dcim.models import Device from utilities.models import CreatedUpdatedModel - from .exceptions import InvalidKey from .hashers import SecretValidationHasher @@ -301,8 +302,8 @@ class Secret(CreatedUpdatedModel): def __str__(self): if self.role and self.device: - return u'{} for {}'.format(self.role, self.device) - return u'Secret' + return '{} for {}'.format(self.role, self.device) + return 'Secret' def get_absolute_url(self): return reverse('secrets:secret', args=[self.pk]) diff --git a/netbox/secrets/tables.py b/netbox/secrets/tables.py index ca9b2fd96..980c093b7 100644 --- a/netbox/secrets/tables.py +++ b/netbox/secrets/tables.py @@ -1,5 +1,6 @@ +from __future__ import unicode_literals + import django_tables2 as tables -from django_tables2.utils import Accessor from utilities.tables import BaseTable, SearchTable, ToggleColumn @@ -22,8 +23,9 @@ class SecretRoleTable(BaseTable): name = tables.LinkColumn(verbose_name='Name') secret_count = tables.Column(verbose_name='Secrets') slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, - verbose_name='') + actions = tables.TemplateColumn( + template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' + ) class Meta(BaseTable.Meta): model = SecretRole diff --git a/netbox/secrets/templatetags/secret_helpers.py b/netbox/secrets/templatetags/secret_helpers.py index 142c0d2cb..0e1ff554c 100644 --- a/netbox/secrets/templatetags/secret_helpers.py +++ b/netbox/secrets/templatetags/secret_helpers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django import template diff --git a/netbox/secrets/tests/test_api.py b/netbox/secrets/tests/test_api.py index 227478b99..4acda3eff 100644 --- a/netbox/secrets/tests/test_api.py +++ b/netbox/secrets/tests/test_api.py @@ -1,4 +1,6 @@ +from __future__ import unicode_literals import base64 + from rest_framework import status from rest_framework.test import APITestCase diff --git a/netbox/secrets/tests/test_models.py b/netbox/secrets/tests/test_models.py index e668f1185..4be37801d 100644 --- a/netbox/secrets/tests/test_models.py +++ b/netbox/secrets/tests/test_models.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from Crypto.PublicKey import RSA from django.conf import settings diff --git a/netbox/secrets/urls.py b/netbox/secrets/urls.py index e801c2de2..4961b2c82 100644 --- a/netbox/secrets/urls.py +++ b/netbox/secrets/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf.urls import url from . import views diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 05fb5c262..d2427dd73 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals import base64 from django.contrib import messages @@ -12,7 +13,6 @@ from django.views.generic import View from dcim.models import Device from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView - from . import filters, forms, tables from .decorators import userkey_required from .models import SecretRole, Secret, SessionKey @@ -110,7 +110,7 @@ def secret_add(request, pk): secret.plaintext = str(form.cleaned_data['plaintext']) secret.encrypt(master_key) secret.save() - messages.success(request, u"Added new secret: {}.".format(secret)) + messages.success(request, "Added new secret: {}.".format(secret)) if '_addanother' in request.POST: return redirect('dcim:device_addsecret', pk=device.pk) else: @@ -154,7 +154,7 @@ def secret_edit(request, pk): secret.plaintext = str(form.cleaned_data['plaintext']) secret.encrypt(master_key) secret.save() - messages.success(request, u"Modified secret {}.".format(secret)) + messages.success(request, "Modified secret {}.".format(secret)) return redirect('secrets:secret', pk=secret.pk) else: form.add_error(None, "Invalid session key. Unable to encrypt secret data.") @@ -166,7 +166,7 @@ def secret_edit(request, pk): # If no new plaintext was specified, a session key is not needed. else: secret = form.save() - messages.success(request, u"Modified secret {}.".format(secret)) + messages.success(request, "Modified secret {}.".format(secret)) return redirect('secrets:secret', pk=secret.pk) else: @@ -220,7 +220,7 @@ def secret_import(request): new_secrets.append(secret) table = tables.SecretTable(new_secrets) - messages.success(request, u"Imported {} new secrets.".format(len(new_secrets))) + messages.success(request, "Imported {} new secrets.".format(len(new_secrets))) return render(request, 'import_success.html', { 'table': table, diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index e649b6f03..712d524c5 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import serializers from extras.api.customfields import CustomFieldModelSerializer diff --git a/netbox/tenancy/api/urls.py b/netbox/tenancy/api/urls.py index f14d39126..a3ce7774a 100644 --- a/netbox/tenancy/api/urls.py +++ b/netbox/tenancy/api/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import routers from . import views diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index ae5069271..e5105f338 100644 --- a/netbox/tenancy/api/views.py +++ b/netbox/tenancy/api/views.py @@ -1,9 +1,10 @@ +from __future__ import unicode_literals + from rest_framework.viewsets import ModelViewSet +from extras.api.views import CustomFieldModelViewSet from tenancy.models import Tenant, TenantGroup from tenancy.filters import TenantFilter - -from extras.api.views import CustomFieldModelViewSet from utilities.api import WritableSerializerMixin from . import serializers diff --git a/netbox/tenancy/apps.py b/netbox/tenancy/apps.py index 53cb9a056..df2cd2fbb 100644 --- a/netbox/tenancy/apps.py +++ b/netbox/tenancy/apps.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.apps import AppConfig diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index b96345980..4ded4f0c4 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import django_filters from django.db.models import Q diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index e9a39d237..518941539 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django import forms from django.db.models import Count diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 203dc7e41..ea3405df9 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.urls import reverse diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 56cf1c02a..9941e269a 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -1,5 +1,6 @@ +from __future__ import unicode_literals + import django_tables2 as tables -from django_tables2.utils import Accessor from utilities.tables import BaseTable, SearchTable, ToggleColumn @@ -22,8 +23,9 @@ class TenantGroupTable(BaseTable): name = tables.LinkColumn(verbose_name='Name') tenant_count = tables.Column(verbose_name='Tenants') slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=TENANTGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}}, - verbose_name='') + actions = tables.TemplateColumn( + template_code=TENANTGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' + ) class Meta(BaseTable.Meta): model = TenantGroup diff --git a/netbox/tenancy/tests/test_api.py b/netbox/tenancy/tests/test_api.py index 11e89fae1..5414a1b06 100644 --- a/netbox/tenancy/tests/test_api.py +++ b/netbox/tenancy/tests/test_api.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import status from rest_framework.test import APITestCase diff --git a/netbox/tenancy/urls.py b/netbox/tenancy/urls.py index be450be7e..cb04cffb9 100644 --- a/netbox/tenancy/urls.py +++ b/netbox/tenancy/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf.urls import url from . import views diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index d520039b4..3b5ad9b37 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib.auth.mixins import PermissionRequiredMixin from django.db.models import Count, Q from django.shortcuts import get_object_or_404, render @@ -10,7 +12,6 @@ from ipam.models import IPAddress, Prefix, VLAN, VRF from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) - from .models import Tenant, TenantGroup from . import filters, forms, tables diff --git a/netbox/users/admin.py b/netbox/users/admin.py index adfa151d5..ccf640edd 100644 --- a/netbox/users/admin.py +++ b/netbox/users/admin.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django import forms from django.contrib import admin diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index 893a989bc..a516b2121 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib.auth.models import User from rest_framework import serializers diff --git a/netbox/users/forms.py b/netbox/users/forms.py index d84bac0e1..811ca6cc5 100644 --- a/netbox/users/forms.py +++ b/netbox/users/forms.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm from django import forms diff --git a/netbox/users/models.py b/netbox/users/models.py index 16c5005ef..a2d9d09ba 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals import binascii import os @@ -26,7 +27,7 @@ class Token(models.Model): def __str__(self): # Only display the last 24 bits of the token to avoid accidental exposure. - return u"{} ({})".format(self.key[-6:], self.user) + return "{} ({})".format(self.key[-6:], self.user) def save(self, *args, **kwargs): if not self.key: diff --git a/netbox/users/urls.py b/netbox/users/urls.py index 3183e5063..57a7e43cc 100644 --- a/netbox/users/urls.py +++ b/netbox/users/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf.urls import url from . import views diff --git a/netbox/users/views.py b/netbox/users/views.py index 15f4bd55f..88b6ebd32 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib import messages from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash from django.contrib.auth.decorators import login_required @@ -41,7 +43,7 @@ class LoginView(View): # Authenticate user auth_login(request, form.get_user()) - messages.info(request, u"Logged in as {}.".format(request.user)) + messages.info(request, "Logged in as {}.".format(request.user)) return HttpResponseRedirect(redirect_to) @@ -54,7 +56,7 @@ class LogoutView(View): def get(self, request): auth_logout(request) - messages.info(request, u"You have logged out.") + messages.info(request, "You have logged out.") return HttpResponseRedirect(reverse('home')) @@ -91,7 +93,7 @@ class ChangePasswordView(View): if form.is_valid(): form.save() update_session_auth_hash(request, form.user) - messages.success(request, u"Your password has been changed successfully.") + messages.success(request, "Your password has been changed successfully.") return redirect('user:profile') return render(request, self.template_name, { @@ -143,7 +145,7 @@ class UserKeyEditView(View): uk = form.save(commit=False) uk.user = request.user uk.save() - messages.success(request, u"Your user key has been saved.") + messages.success(request, "Your user key has been saved.") return redirect('user:userkey') return render(request, self.template_name, { diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index ebcb19d44..a587c67d1 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf import settings from django.contrib.contenttypes.models import ContentType diff --git a/netbox/utilities/context_processors.py b/netbox/utilities/context_processors.py index 6b1bb0af5..58c8641ec 100644 --- a/netbox/utilities/context_processors.py +++ b/netbox/utilities/context_processors.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf import settings as django_settings diff --git a/netbox/utilities/error_handlers.py b/netbox/utilities/error_handlers.py index e87b6f0e1..3b7eb7a5b 100644 --- a/netbox/utilities/error_handlers.py +++ b/netbox/utilities/error_handlers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib import messages from django.utils.html import escape from django.utils.safestring import mark_safe @@ -14,12 +16,12 @@ def handle_protectederror(obj, request, e): # Grammar for single versus multiple triggering objects if type(obj) in (list, tuple): - err_message = u"Unable to delete the requested {}. The following dependent {} were found: ".format( + err_message = "Unable to delete the requested {}. The following dependent {} were found: ".format( obj[0]._meta.verbose_name_plural, dep_class, ) else: - err_message = u"Unable to delete {} {}. The following dependent {} were found: ".format( + err_message = "Unable to delete {} {}. The following dependent {} were found: ".format( obj._meta.verbose_name, obj, dep_class, @@ -29,9 +31,9 @@ def handle_protectederror(obj, request, e): dependent_objects = [] for obj in e.protected_objects: if hasattr(obj, 'get_absolute_url'): - dependent_objects.append(u'{}'.format(obj.get_absolute_url(), escape(obj))) + dependent_objects.append('{}'.format(obj.get_absolute_url(), escape(obj))) else: dependent_objects.append(str(obj)) - err_message += u', '.join(dependent_objects) + err_message += ', '.join(dependent_objects) messages.error(request, mark_safe(err_message)) diff --git a/netbox/utilities/fields.py b/netbox/utilities/fields.py index 14d1c7d8f..2b1a51048 100644 --- a/netbox/utilities/fields.py +++ b/netbox/utilities/fields.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.core.validators import RegexValidator from django.db import models diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py index c352f0f41..5929c3ff1 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import django_filters import itertools diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index ba97a6507..c484f027b 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals import csv import itertools import re @@ -373,7 +374,7 @@ class FilterChoiceFieldMixin(object): def label_from_instance(self, obj): label = super(FilterChoiceFieldMixin, self).label_from_instance(obj) if hasattr(obj, 'filter_count'): - return u'{} ({})'.format(label, obj.filter_count) + return '{} ({})'.format(label, obj.filter_count) return label def _get_choices(self): diff --git a/netbox/utilities/managers.py b/netbox/utilities/managers.py index 5b6138d30..dc882d462 100644 --- a/netbox/utilities/managers.py +++ b/netbox/utilities/managers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.db.models import Manager diff --git a/netbox/utilities/middleware.py b/netbox/utilities/middleware.py index 22697d179..d5d151c12 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.http import HttpResponseRedirect from django.conf import settings from django.urls import reverse diff --git a/netbox/utilities/models.py b/netbox/utilities/models.py index 88f513554..c6768c4c1 100644 --- a/netbox/utilities/models.py +++ b/netbox/utilities/models.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.db import models diff --git a/netbox/utilities/paginator.py b/netbox/utilities/paginator.py index ae915f773..9ebbbab57 100644 --- a/netbox/utilities/paginator.py +++ b/netbox/utilities/paginator.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf import settings from django.core.paginator import Paginator, Page diff --git a/netbox/utilities/sql.py b/netbox/utilities/sql.py index 617586ab8..ac2c70624 100644 --- a/netbox/utilities/sql.py +++ b/netbox/utilities/sql.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.db import connections, models from django.db.models.sql.compiler import SQLCompiler diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index 1c5eab2a6..1dd8969a1 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import django_tables2 as tables from django.utils.safestring import mark_safe diff --git a/netbox/utilities/tests.py b/netbox/utilities/tests.py index d80b42c63..3c7f51eba 100644 --- a/netbox/utilities/tests.py +++ b/netbox/utilities/tests.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + class HttpStatusMixin(object): """ Custom mixin to provide more detail in the event of an unexpected HTTP response. diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 98400bccd..3e9364419 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals import six @@ -10,20 +11,20 @@ def csv_format(data): # Represent None or False with empty string if value in [None, False]: - csv.append(u'') + csv.append('') continue # Force conversion to string first so we can check for any commas if not isinstance(value, six.string_types): - value = u'{}'.format(value) + value = '{}'.format(value) # Double-quote the value if it contains a comma - if u',' in value: - csv.append(u'"{}"'.format(value)) + if ',' in value: + csv.append('"{}"'.format(value)) else: - csv.append(u'{}'.format(value)) + csv.append('{}'.format(value)) - return u','.join(csv) + return ','.join(csv) def foreground_color(bg_color): diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index d61a55e66..9c7a4b55e 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -1,4 +1,6 @@ +from __future__ import unicode_literals from collections import OrderedDict + from django_tables2 import RequestConfig from django.conf import settings @@ -17,7 +19,6 @@ from django.utils.safestring import mark_safe from django.views.generic import View from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction - from .error_handlers import handle_protectederror from .forms import ConfirmationForm from .paginator import EnhancedPaginator @@ -95,7 +96,7 @@ class ObjectListView(View): filename='netbox_{}'.format(model._meta.verbose_name_plural)) return response except TemplateSyntaxError: - messages.error(request, u"There was an error rendering the selected export template ({})." + messages.error(request, "There was an error rendering the selected export template ({})." .format(et.name)) # Fall back to built-in CSV export elif 'export' in request.GET and hasattr(model, 'to_csv'): @@ -196,12 +197,12 @@ class ObjectEditView(GetReturnURLMixin, View): obj_created = not form.instance.pk obj = form.save() - msg = u'Created ' if obj_created else u'Modified ' + msg = 'Created ' if obj_created else 'Modified ' msg += self.model._meta.verbose_name if hasattr(obj, 'get_absolute_url'): - msg = u'{} {}'.format(msg, obj.get_absolute_url(), escape(obj)) + msg = '{} {}'.format(msg, obj.get_absolute_url(), escape(obj)) else: - msg = u'{} {}'.format(msg, escape(obj)) + msg = '{} {}'.format(msg, escape(obj)) messages.success(request, mark_safe(msg)) if obj_created: UserAction.objects.log_create(request.user, obj, msg) @@ -267,7 +268,7 @@ class ObjectDeleteView(GetReturnURLMixin, View): handle_protectederror(obj, request, e) return redirect(obj.get_absolute_url()) - msg = u'Deleted {} {}'.format(self.model._meta.verbose_name, obj) + msg = 'Deleted {} {}'.format(self.model._meta.verbose_name, obj) messages.success(request, msg) UserAction.objects.log_delete(request.user, obj, msg) @@ -347,7 +348,7 @@ class BulkAddView(View): raise IntegrityError() # If we make it to this point, validation has succeeded on all new objects. - msg = u"Added {} {}".format(len(new_objs), model._meta.verbose_name_plural) + msg = "Added {} {}".format(len(new_objs), model._meta.verbose_name_plural) messages.success(request, msg) UserAction.objects.log_bulk_create(request.user, ContentType.objects.get_for_model(model), msg) @@ -400,7 +401,7 @@ class BulkImportView(View): obj_table = self.table(new_objs) if new_objs: - msg = u'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural) + msg = 'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural) messages.success(request, msg) UserAction.objects.log_import(request.user, ContentType.objects.get_for_model(new_objs[0]), msg) @@ -493,7 +494,7 @@ class BulkEditView(View): updated_count = objs_updated if updated_count: - msg = u'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural) + msg = 'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural) messages.success(self.request, msg) UserAction.objects.log_bulk_edit(request.user, ContentType.objects.get_for_model(self.cls), msg) return redirect(return_url) @@ -505,7 +506,7 @@ class BulkEditView(View): selected_objects = self.cls.objects.filter(pk__in=pk_list) if not selected_objects: - messages.warning(request, u"No {} were selected.".format(self.cls._meta.verbose_name_plural)) + messages.warning(request, "No {} were selected.".format(self.cls._meta.verbose_name_plural)) return redirect(return_url) return render(request, self.template_name, { @@ -530,7 +531,7 @@ class BulkEditView(View): objs_updated = True # Updating the value of the field - elif form.cleaned_data[name] not in [None, u'']: + elif form.cleaned_data[name] not in [None, '']: # Check for zero value (bulk editing) if isinstance(form.fields[name], TypedChoiceField) and form.cleaned_data[name] == 0: @@ -618,7 +619,7 @@ class BulkDeleteView(View): handle_protectederror(list(queryset), request, e) return redirect(return_url) - msg = u'Deleted {} {}'.format(deleted_count, self.cls._meta.verbose_name_plural) + msg = 'Deleted {} {}'.format(deleted_count, self.cls._meta.verbose_name_plural) messages.success(request, msg) UserAction.objects.log_bulk_delete(request.user, ContentType.objects.get_for_model(self.cls), msg) return redirect(return_url) @@ -628,7 +629,7 @@ class BulkDeleteView(View): selected_objects = self.cls.objects.filter(pk__in=pk_list) if not selected_objects: - messages.warning(request, u"No {} were selected for deletion.".format(self.cls._meta.verbose_name_plural)) + messages.warning(request, "No {} were selected for deletion.".format(self.cls._meta.verbose_name_plural)) return redirect(return_url) return render(request, self.template_name, { From 138cbf9761024a009d315a9a1f2ad9db16b7022a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 24 May 2017 14:18:52 -0400 Subject: [PATCH 16/20] Created migrations for transition to Unicode literals --- .../migrations/0009_unicode_literals.py | 81 +++++++ .../dcim/migrations/0037_unicode_literals.py | 209 ++++++++++++++++++ .../migrations/0007_unicode_literals.py | 91 ++++++++ .../ipam/migrations/0016_unicode_literals.py | 133 +++++++++++ .../migrations/0003_unicode_literals.py | 20 ++ .../migrations/0003_unicode_literals.py | 20 ++ .../users/migrations/0002_unicode_literals.py | 20 ++ 7 files changed, 574 insertions(+) create mode 100644 netbox/circuits/migrations/0009_unicode_literals.py create mode 100644 netbox/dcim/migrations/0037_unicode_literals.py create mode 100644 netbox/extras/migrations/0007_unicode_literals.py create mode 100644 netbox/ipam/migrations/0016_unicode_literals.py create mode 100644 netbox/secrets/migrations/0003_unicode_literals.py create mode 100644 netbox/tenancy/migrations/0003_unicode_literals.py create mode 100644 netbox/users/migrations/0002_unicode_literals.py diff --git a/netbox/circuits/migrations/0009_unicode_literals.py b/netbox/circuits/migrations/0009_unicode_literals.py new file mode 100644 index 000000000..0f22a2268 --- /dev/null +++ b/netbox/circuits/migrations/0009_unicode_literals.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-24 15:34 +from __future__ import unicode_literals + +import dcim.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0008_circuittermination_interface_protect_on_delete'), + ] + + operations = [ + migrations.AlterField( + model_name='circuit', + name='cid', + field=models.CharField(max_length=50, verbose_name='Circuit ID'), + ), + migrations.AlterField( + model_name='circuit', + name='commit_rate', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)'), + ), + migrations.AlterField( + model_name='circuit', + name='install_date', + field=models.DateField(blank=True, null=True, verbose_name='Date installed'), + ), + migrations.AlterField( + model_name='circuittermination', + name='port_speed', + field=models.PositiveIntegerField(verbose_name='Port speed (Kbps)'), + ), + migrations.AlterField( + model_name='circuittermination', + name='pp_info', + field=models.CharField(blank=True, max_length=100, verbose_name='Patch panel/port(s)'), + ), + migrations.AlterField( + model_name='circuittermination', + name='term_side', + field=models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination'), + ), + migrations.AlterField( + model_name='circuittermination', + name='upstream_speed', + field=models.PositiveIntegerField(blank=True, help_text='Upstream speed, if different from port speed', null=True, verbose_name='Upstream speed (Kbps)'), + ), + migrations.AlterField( + model_name='circuittermination', + name='xconnect_id', + field=models.CharField(blank=True, max_length=50, verbose_name='Cross-connect ID'), + ), + migrations.AlterField( + model_name='provider', + name='account', + field=models.CharField(blank=True, max_length=30, verbose_name='Account number'), + ), + migrations.AlterField( + model_name='provider', + name='admin_contact', + field=models.TextField(blank=True, verbose_name='Admin contact'), + ), + migrations.AlterField( + model_name='provider', + name='asn', + field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'), + ), + migrations.AlterField( + model_name='provider', + name='noc_contact', + field=models.TextField(blank=True, verbose_name='NOC contact'), + ), + migrations.AlterField( + model_name='provider', + name='portal_url', + field=models.URLField(blank=True, verbose_name='Portal'), + ), + ] diff --git a/netbox/dcim/migrations/0037_unicode_literals.py b/netbox/dcim/migrations/0037_unicode_literals.py new file mode 100644 index 000000000..cba05becc --- /dev/null +++ b/netbox/dcim/migrations/0037_unicode_literals.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-24 15:34 +from __future__ import unicode_literals + +import dcim.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import utilities.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0036_add_ff_juniper_vcp'), + ] + + operations = [ + migrations.AlterField( + model_name='consoleport', + name='connection_status', + field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True), + ), + migrations.AlterField( + model_name='consoleport', + name='cs_port', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name='Console server port'), + ), + migrations.AlterField( + model_name='device', + name='asset_tag', + field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name='Asset tag'), + ), + migrations.AlterField( + model_name='device', + name='face', + field=models.PositiveSmallIntegerField(blank=True, choices=[[0, 'Front'], [1, 'Rear']], null=True, verbose_name='Rack face'), + ), + migrations.AlterField( + model_name='device', + name='position', + field=models.PositiveSmallIntegerField(blank=True, help_text='The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Position (U)'), + ), + migrations.AlterField( + model_name='device', + name='primary_ip4', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name='Primary IPv4'), + ), + migrations.AlterField( + model_name='device', + name='primary_ip6', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name='Primary IPv6'), + ), + migrations.AlterField( + model_name='device', + name='serial', + field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'), + ), + migrations.AlterField( + model_name='device', + name='status', + field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [2, 'Planned'], [3, 'Staged'], [4, 'Failed'], [5, 'Inventory']], default=1, verbose_name='Status'), + ), + migrations.AlterField( + model_name='devicebay', + name='name', + field=models.CharField(max_length=50, verbose_name='Name'), + ), + migrations.AlterField( + model_name='devicetype', + name='interface_ordering', + field=models.PositiveSmallIntegerField(choices=[[1, 'Slot/position'], [2, 'Name (alphabetically)']], default=1), + ), + migrations.AlterField( + model_name='devicetype', + name='is_console_server', + field=models.BooleanField(default=False, help_text='This type of device has console server ports', verbose_name='Is a console server'), + ), + migrations.AlterField( + model_name='devicetype', + name='is_full_depth', + field=models.BooleanField(default=True, help_text='Device consumes both front and rear rack faces', verbose_name='Is full depth'), + ), + migrations.AlterField( + model_name='devicetype', + name='is_network_device', + field=models.BooleanField(default=True, help_text='This type of device has network interfaces', verbose_name='Is a network device'), + ), + migrations.AlterField( + model_name='devicetype', + name='is_pdu', + field=models.BooleanField(default=False, help_text='This type of device has power outlets', verbose_name='Is a PDU'), + ), + migrations.AlterField( + model_name='devicetype', + name='part_number', + field=models.CharField(blank=True, help_text='Discrete part number (optional)', max_length=50), + ), + migrations.AlterField( + model_name='devicetype', + name='subdevice_role', + field=models.NullBooleanField(choices=[(None, 'None'), (True, 'Parent'), (False, 'Child')], default=None, help_text='Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name='Parent/child status'), + ), + migrations.AlterField( + model_name='devicetype', + name='u_height', + field=models.PositiveSmallIntegerField(default=1, verbose_name='Height (U)'), + ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interface', + name='lag', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name='Parent LAG'), + ), + migrations.AlterField( + model_name='interface', + name='mac_address', + field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name='MAC Address'), + ), + migrations.AlterField( + model_name='interface', + name='mgmt_only', + field=models.BooleanField(default=False, help_text='This interface is used only for out-of-band management', verbose_name='OOB Management'), + ), + migrations.AlterField( + model_name='interfaceconnection', + name='connection_status', + field=models.BooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True, verbose_name='Status'), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='mgmt_only', + field=models.BooleanField(default=False, verbose_name='Management only'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='discovered', + field=models.BooleanField(default=False, verbose_name='Discovered'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='name', + field=models.CharField(max_length=50, verbose_name='Name'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='part_id', + field=models.CharField(blank=True, max_length=50, verbose_name='Part ID'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='serial', + field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'), + ), + migrations.AlterField( + model_name='platform', + name='rpc_client', + field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='RPC client'), + ), + migrations.AlterField( + model_name='powerport', + name='connection_status', + field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True), + ), + migrations.AlterField( + model_name='rack', + name='desc_units', + field=models.BooleanField(default=False, help_text='Units are numbered top-to-bottom', verbose_name='Descending units'), + ), + migrations.AlterField( + model_name='rack', + name='facility_id', + field=utilities.fields.NullableCharField(blank=True, max_length=30, null=True, verbose_name='Facility ID'), + ), + migrations.AlterField( + model_name='rack', + name='type', + field=models.PositiveSmallIntegerField(blank=True, choices=[(100, '2-post frame'), (200, '4-post frame'), (300, '4-post cabinet'), (1000, 'Wall-mounted frame'), (1100, 'Wall-mounted cabinet')], null=True, verbose_name='Type'), + ), + migrations.AlterField( + model_name='rack', + name='u_height', + field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Height (U)'), + ), + migrations.AlterField( + model_name='rack', + name='width', + field=models.PositiveSmallIntegerField(choices=[(19, '19 inches'), (23, '23 inches')], default=19, help_text='Rail-to-rail width', verbose_name='Width'), + ), + migrations.AlterField( + model_name='site', + name='asn', + field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'), + ), + migrations.AlterField( + model_name='site', + name='contact_email', + field=models.EmailField(blank=True, max_length=254, verbose_name='Contact E-mail'), + ), + ] diff --git a/netbox/extras/migrations/0007_unicode_literals.py b/netbox/extras/migrations/0007_unicode_literals.py new file mode 100644 index 000000000..c9a624510 --- /dev/null +++ b/netbox/extras/migrations/0007_unicode_literals.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-24 15:34 +from __future__ import unicode_literals + +from django.db import migrations, models +import extras.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0006_add_imageattachments'), + ] + + operations = [ + migrations.AlterField( + model_name='customfield', + name='default', + field=models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.', max_length=100), + ), + migrations.AlterField( + model_name='customfield', + name='is_filterable', + field=models.BooleanField(default=True, help_text='This field can be used to filter objects.'), + ), + migrations.AlterField( + model_name='customfield', + name='label', + field=models.CharField(blank=True, help_text="Name of the field as displayed to users (if not provided, the field's name will be used)", max_length=50), + ), + migrations.AlterField( + model_name='customfield', + name='obj_type', + field=models.ManyToManyField(help_text='The object(s) to which this field applies.', related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'), + ), + migrations.AlterField( + model_name='customfield', + name='required', + field=models.BooleanField(default=False, help_text='Determines whether this field is required when creating new objects or editing an existing object.'), + ), + migrations.AlterField( + model_name='customfield', + name='type', + field=models.PositiveSmallIntegerField(choices=[(100, 'Text'), (200, 'Integer'), (300, 'Boolean (true/false)'), (400, 'Date'), (500, 'URL'), (600, 'Selection')], default=100), + ), + migrations.AlterField( + model_name='customfield', + name='weight', + field=models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form'), + ), + migrations.AlterField( + model_name='customfieldchoice', + name='weight', + field=models.PositiveSmallIntegerField(default=100, help_text='Higher weights appear lower in the list'), + ), + migrations.AlterField( + model_name='graph', + name='link', + field=models.URLField(blank=True, verbose_name='Link URL'), + ), + migrations.AlterField( + model_name='graph', + name='name', + field=models.CharField(max_length=100, verbose_name='Name'), + ), + migrations.AlterField( + model_name='graph', + name='source', + field=models.CharField(max_length=500, verbose_name='Source URL'), + ), + migrations.AlterField( + model_name='graph', + name='type', + field=models.PositiveSmallIntegerField(choices=[(100, 'Interface'), (200, 'Provider'), (300, 'Site')]), + ), + migrations.AlterField( + model_name='imageattachment', + name='image', + field=models.ImageField(height_field='image_height', upload_to=extras.models.image_upload, width_field='image_width'), + ), + migrations.AlterField( + model_name='topologymap', + name='device_patterns', + field=models.TextField(help_text='Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.'), + ), + migrations.AlterField( + model_name='useraction', + name='action', + field=models.PositiveSmallIntegerField(choices=[(1, 'created'), (7, 'bulk created'), (2, 'imported'), (3, 'modified'), (4, 'bulk edited'), (5, 'deleted'), (6, 'bulk deleted')]), + ), + ] diff --git a/netbox/ipam/migrations/0016_unicode_literals.py b/netbox/ipam/migrations/0016_unicode_literals.py new file mode 100644 index 000000000..bb29542ad --- /dev/null +++ b/netbox/ipam/migrations/0016_unicode_literals.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-24 15:34 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import ipam.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0015_global_vlans'), + ] + + operations = [ + migrations.AlterField( + model_name='aggregate', + name='family', + field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')]), + ), + migrations.AlterField( + model_name='aggregate', + name='rir', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='aggregates', to='ipam.RIR', verbose_name='RIR'), + ), + migrations.AlterField( + model_name='ipaddress', + name='address', + field=ipam.fields.IPAddressField(help_text='IPv4 or IPv6 address (with mask)'), + ), + migrations.AlterField( + model_name='ipaddress', + name='family', + field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')], editable=False), + ), + migrations.AlterField( + model_name='ipaddress', + name='nat_inside', + field=models.OneToOneField(blank=True, help_text='The IP for which this address is the "outside" IP', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nat_outside', to='ipam.IPAddress', verbose_name='NAT (Inside)'), + ), + migrations.AlterField( + model_name='ipaddress', + name='status', + field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated'), (5, 'DHCP')], default=1, verbose_name='Status'), + ), + migrations.AlterField( + model_name='ipaddress', + name='vrf', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='ipam.VRF', verbose_name='VRF'), + ), + migrations.AlterField( + model_name='prefix', + name='family', + field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')], editable=False), + ), + migrations.AlterField( + model_name='prefix', + name='is_pool', + field=models.BooleanField(default=False, help_text='All IP addresses within this prefix are considered usable', verbose_name='Is a pool'), + ), + migrations.AlterField( + model_name='prefix', + name='prefix', + field=ipam.fields.IPNetworkField(help_text='IPv4 or IPv6 network with mask'), + ), + migrations.AlterField( + model_name='prefix', + name='role', + field=models.ForeignKey(blank=True, help_text='The primary function of this prefix', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prefixes', to='ipam.Role'), + ), + migrations.AlterField( + model_name='prefix', + name='status', + field=models.PositiveSmallIntegerField(choices=[(0, 'Container'), (1, 'Active'), (2, 'Reserved'), (3, 'Deprecated')], default=1, help_text='Operational status of this prefix', verbose_name='Status'), + ), + migrations.AlterField( + model_name='prefix', + name='vlan', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VLAN', verbose_name='VLAN'), + ), + migrations.AlterField( + model_name='prefix', + name='vrf', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VRF', verbose_name='VRF'), + ), + migrations.AlterField( + model_name='rir', + name='is_private', + field=models.BooleanField(default=False, help_text='IP space managed by this RIR is considered private', verbose_name='Private'), + ), + migrations.AlterField( + model_name='service', + name='device', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='dcim.Device', verbose_name='device'), + ), + migrations.AlterField( + model_name='service', + name='ipaddresses', + field=models.ManyToManyField(blank=True, related_name='services', to='ipam.IPAddress', verbose_name='IP addresses'), + ), + migrations.AlterField( + model_name='service', + name='port', + field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)], verbose_name='Port number'), + ), + migrations.AlterField( + model_name='service', + name='protocol', + field=models.PositiveSmallIntegerField(choices=[(6, 'TCP'), (17, 'UDP')]), + ), + migrations.AlterField( + model_name='vlan', + name='status', + field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated')], default=1, verbose_name='Status'), + ), + migrations.AlterField( + model_name='vlan', + name='vid', + field=models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)], verbose_name='ID'), + ), + migrations.AlterField( + model_name='vrf', + name='enforce_unique', + field=models.BooleanField(default=True, help_text='Prevent duplicate prefixes/IP addresses within this VRF', verbose_name='Enforce unique space'), + ), + migrations.AlterField( + model_name='vrf', + name='rd', + field=models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher'), + ), + ] diff --git a/netbox/secrets/migrations/0003_unicode_literals.py b/netbox/secrets/migrations/0003_unicode_literals.py new file mode 100644 index 000000000..b8b7956d8 --- /dev/null +++ b/netbox/secrets/migrations/0003_unicode_literals.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-24 15:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('secrets', '0002_userkey_add_session_key'), + ] + + operations = [ + migrations.AlterField( + model_name='userkey', + name='public_key', + field=models.TextField(verbose_name='RSA public key'), + ), + ] diff --git a/netbox/tenancy/migrations/0003_unicode_literals.py b/netbox/tenancy/migrations/0003_unicode_literals.py new file mode 100644 index 000000000..ed547c510 --- /dev/null +++ b/netbox/tenancy/migrations/0003_unicode_literals.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-24 15:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0002_tenant_group_optional'), + ] + + operations = [ + migrations.AlterField( + model_name='tenant', + name='description', + field=models.CharField(blank=True, help_text='Long-form name (optional)', max_length=100), + ), + ] diff --git a/netbox/users/migrations/0002_unicode_literals.py b/netbox/users/migrations/0002_unicode_literals.py new file mode 100644 index 000000000..8a7f96bbd --- /dev/null +++ b/netbox/users/migrations/0002_unicode_literals.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-24 15:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_api_tokens'), + ] + + operations = [ + migrations.AlterField( + model_name='token', + name='write_enabled', + field=models.BooleanField(default=True, help_text='Permit create/update/delete operations using this key'), + ), + ] From ebddc46bc0c379d1d4f6acaec08a2a43d79ccdf2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 24 May 2017 14:22:37 -0400 Subject: [PATCH 17/20] PEP8 fix --- netbox/utilities/tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/utilities/tests.py b/netbox/utilities/tests.py index 3c7f51eba..d40202842 100644 --- a/netbox/utilities/tests.py +++ b/netbox/utilities/tests.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals + class HttpStatusMixin(object): """ Custom mixin to provide more detail in the event of an unexpected HTTP response. From 1dd5e2c926fd2042122d77e3b7ab74456c4f7791 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 25 May 2017 14:33:50 -0400 Subject: [PATCH 18/20] Fixes #1229: Fix validation error on forms where API search is used --- netbox/circuits/forms.py | 28 ++--- netbox/dcim/forms.py | 116 +++++++++++++----- netbox/dcim/views.py | 34 ++--- netbox/ipam/forms.py | 55 ++++++--- .../circuits/circuittermination_edit.html | 19 +-- .../templates/dcim/consoleport_connect.html | 7 +- .../dcim/consoleserverport_connect.html | 7 +- .../templates/dcim/poweroutlet_connect.html | 7 +- netbox/templates/dcim/powerport_connect.html | 7 +- netbox/templates/ipam/ipaddress_edit.html | 1 + netbox/tenancy/forms.py | 4 +- netbox/utilities/forms.py | 6 +- 12 files changed, 173 insertions(+), 118 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index eb38b8102..79cad0a6b 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -167,7 +167,9 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm ) rack = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'site'}, + chains=( + ('site', 'site'), + ), required=False, label='Rack', widget=APISelect( @@ -177,7 +179,10 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm ) device = ChainedModelChoiceField( queryset=Device.objects.all(), - chains={'site': 'site', 'rack': 'rack'}, + chains=( + ('site', 'site'), + ('rack', 'rack'), + ), required=False, label='Device', widget=APISelect( @@ -186,20 +191,13 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm attrs={'filter-for': 'interface'} ) ) - livesearch = forms.CharField( - required=False, - label='Device', - widget=Livesearch( - query_key='q', - query_url='dcim-api:device-list', - field_to_update='device' - ) - ) interface = ChainedModelChoiceField( queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related( 'circuit_termination', 'connected_as_a', 'connected_as_b' ), - chains={'device': 'device'}, + chains=( + ('device', 'device'), + ), required=False, label='Interface', widget=APISelect( @@ -210,8 +208,10 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm class Meta: model = CircuitTermination - fields = ['term_side', 'site', 'rack', 'device', 'livesearch', 'interface', 'port_speed', 'upstream_speed', - 'xconnect_id', 'pp_info'] + fields = [ + 'term_side', 'site', 'rack', 'device', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', + 'pp_info', + ] help_texts = { 'port_speed': "Physical circuit speed", 'xconnect_id': "ID of the local cross-connect", diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 03fddf21d..9e1cc657d 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -190,7 +190,9 @@ class RackRoleForm(BootstrapMixin, forms.ModelForm): class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm): group = ChainedModelChoiceField( queryset=RackGroup.objects.all(), - chains={'site': 'site'}, + chains=( + ('site', 'site'), + ), required=False, widget=APISelect( api_url='/api/dcim/rack-groups/?site_id={{site}}', @@ -545,7 +547,9 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): ) rack = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'site'}, + chains=( + ('site', 'site'), + ), required=False, widget=APISelect( api_url='/api/dcim/racks/?site_id={{site}}', @@ -570,7 +574,9 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): ) device_type = ChainedModelChoiceField( queryset=DeviceType.objects.all(), - chains={'manufacturer': 'manufacturer'}, + chains=( + ('manufacturer', 'manufacturer'), + ), label='Device type', widget=APISelect( api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}', @@ -957,20 +963,29 @@ class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm): class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), - widget=forms.HiddenInput(), + required=False, + widget=forms.Select( + attrs={'filter-for': 'rack'} + ) ) rack = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'site'}, + chains=( + ('site', 'site'), + ), label='Rack', required=False, - widget=forms.Select( + widget=APISelect( + api_url='/api/dcim/racks/?site_id={{site}}', attrs={'filter-for': 'console_server', 'nullable': 'true'} ) ) console_server = ChainedModelChoiceField( queryset=Device.objects.filter(device_type__is_console_server=True), - chains={'site': 'site', 'rack': 'rack'}, + chains=( + ('site', 'site'), + ('rack', 'rack'), + ), label='Console Server', required=False, widget=APISelect( @@ -990,7 +1005,9 @@ class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelF ) cs_port = ChainedModelChoiceField( queryset=ConsoleServerPort.objects.all(), - chains={'device': 'console_server'}, + chains=( + ('device', 'console_server'), + ), label='Port', widget=APISelect( api_url='/api/dcim/console-server-ports/?device_id={{console_server}}', @@ -1035,20 +1052,29 @@ class ConsoleServerPortCreateForm(DeviceComponentForm): class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): site = forms.ModelChoiceField( queryset=Site.objects.all(), - widget=forms.HiddenInput(), + required=False, + widget=forms.Select( + attrs={'filter-for': 'rack'} + ) ) rack = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'site'}, + chains=( + ('site', 'site'), + ), label='Rack', required=False, - widget=forms.Select( + widget=APISelect( + api_url='/api/dcim/racks/?site_id={{site}}', attrs={'filter-for': 'device', 'nullable': 'true'} ) ) device = ChainedModelChoiceField( queryset=Device.objects.all(), - chains={'site': 'site', 'rack': 'rack'}, + chains=( + ('site', 'site'), + ('rack', 'rack'), + ), label='Device', required=False, widget=APISelect( @@ -1068,7 +1094,9 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms. ) port = ChainedModelChoiceField( queryset=ConsolePort.objects.all(), - chains={'device': 'device'}, + chains=( + ('device', 'device'), + ), label='Port', widget=APISelect( api_url='/api/dcim/console-ports/?device_id={{device}}', @@ -1182,19 +1210,31 @@ class PowerConnectionImportForm(BootstrapMixin, BulkImportForm): class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): - site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.HiddenInput()) - rack = ChainedModelChoiceField( - queryset=Rack.objects.all(), - chains={'site': 'site'}, - label='Rack', + site = forms.ModelChoiceField( + queryset=Site.objects.all(), required=False, widget=forms.Select( + attrs={'filter-for': 'rack'} + ) + ) + rack = ChainedModelChoiceField( + queryset=Rack.objects.all(), + chains=( + ('site', 'site'), + ), + label='Rack', + required=False, + widget=APISelect( + api_url='/api/dcim/racks/?site_id={{site}}', attrs={'filter-for': 'pdu', 'nullable': 'true'} ) ) pdu = ChainedModelChoiceField( queryset=Device.objects.all(), - chains={'site': 'site', 'rack': 'rack'}, + chains=( + ('site', 'site'), + ('rack', 'rack'), + ), label='PDU', required=False, widget=APISelect( @@ -1214,7 +1254,9 @@ class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor ) power_outlet = ChainedModelChoiceField( queryset=PowerOutlet.objects.all(), - chains={'device': 'pdu'}, + chains=( + ('device', 'pdu'), + ), label='Outlet', widget=APISelect( api_url='/api/dcim/power-outlets/?device_id={{pdu}}', @@ -1259,20 +1301,29 @@ class PowerOutletCreateForm(DeviceComponentForm): class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): site = forms.ModelChoiceField( queryset=Site.objects.all(), - widget=forms.HiddenInput() + required=False, + widget=forms.Select( + attrs={'filter-for': 'rack'} + ) ) rack = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'site'}, + chains=( + ('site', 'site'), + ), label='Rack', required=False, - widget=forms.Select( + widget=APISelect( + api_url='/api/dcim/racks/?site_id={{site}}', attrs={'filter-for': 'device', 'nullable': 'true'} ) ) device = ChainedModelChoiceField( queryset=Device.objects.all(), - chains={'site': 'site', 'rack': 'rack'}, + chains=( + ('site', 'site'), + ('rack', 'rack'), + ), label='Device', required=False, widget=APISelect( @@ -1292,7 +1343,9 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): ) port = ChainedModelChoiceField( queryset=PowerPort.objects.all(), - chains={'device': 'device'}, + chains=( + ('device', 'device'), + ), label='Port', widget=APISelect( api_url='/api/dcim/power-ports/?device_id={{device}}', @@ -1412,7 +1465,9 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor ) rack_b = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'site_b'}, + chains=( + ('site', 'site_b'), + ), label='Rack', required=False, widget=APISelect( @@ -1422,7 +1477,10 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor ) device_b = ChainedModelChoiceField( queryset=Device.objects.all(), - chains={'site': 'site_b', 'rack': 'rack_b'}, + chains=( + ('site', 'site_b'), + ('rack', 'rack_b'), + ), label='Device', required=False, widget=APISelect( @@ -1444,7 +1502,9 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related( 'circuit_termination', 'connected_as_a', 'connected_as_b' ), - chains={'device': 'device_b'}, + chains=( + ('device', 'device_b'), + ), label='Interface', widget=APISelect( api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 322e88b72..f6e00be04 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -944,9 +944,9 @@ def consoleport_connect(request, pk): else: form = forms.ConsolePortConnectionForm(instance=consoleport, initial={ - 'site': request.GET.get('site', consoleport.device.site), - 'rack': request.GET.get('rack', None), - 'console_server': request.GET.get('console_server', None), + 'site': request.GET.get('site'), + 'rack': request.GET.get('rack'), + 'console_server': request.GET.get('console_server'), 'connection_status': CONNECTION_STATUS_CONNECTED, }) @@ -1061,9 +1061,9 @@ def consoleserverport_connect(request, pk): else: form = forms.ConsoleServerPortConnectionForm(initial={ - 'site': request.GET.get('site', consoleserverport.device.site), - 'rack': request.GET.get('rack', None), - 'device': request.GET.get('device', None), + 'site': request.GET.get('site'), + 'rack': request.GET.get('rack'), + 'device': request.GET.get('device'), 'connection_status': CONNECTION_STATUS_CONNECTED, }) @@ -1167,9 +1167,9 @@ def powerport_connect(request, pk): else: form = forms.PowerPortConnectionForm(instance=powerport, initial={ - 'site': request.GET.get('site', powerport.device.site), - 'rack': request.GET.get('rack', None), - 'pdu': request.GET.get('pdu', None), + 'site': request.GET.get('site'), + 'rack': request.GET.get('rack'), + 'pdu': request.GET.get('pdu'), 'connection_status': CONNECTION_STATUS_CONNECTED, }) @@ -1284,9 +1284,9 @@ def poweroutlet_connect(request, pk): else: form = forms.PowerOutletConnectionForm(initial={ - 'site': request.GET.get('site', poweroutlet.device.site), - 'rack': request.GET.get('rack', None), - 'device': request.GET.get('device', None), + 'site': request.GET.get('site'), + 'rack': request.GET.get('rack'), + 'device': request.GET.get('device'), 'connection_status': CONNECTION_STATUS_CONNECTED, }) @@ -1616,11 +1616,11 @@ def interfaceconnection_add(request, pk): else: form = forms.InterfaceConnectionForm(device, initial={ - 'interface_a': request.GET.get('interface_a', None), - 'site_b': request.GET.get('site_b', device.site), - 'rack_b': request.GET.get('rack_b', None), - 'device_b': request.GET.get('device_b', None), - 'interface_b': request.GET.get('interface_b', None), + 'interface_a': request.GET.get('interface_a'), + 'site_b': request.GET.get('site_b'), + 'rack_b': request.GET.get('rack_b'), + 'device_b': request.GET.get('device_b'), + 'interface_b': request.GET.get('interface_b'), }) return render(request, 'dcim/interfaceconnection_edit.html', { diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 7f62015af..3bc8124ea 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -168,12 +168,21 @@ class RoleForm(BootstrapMixin, forms.ModelForm): class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm): site = forms.ModelChoiceField( - queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select( + queryset=Site.objects.all(), + required=False, + label='Site', + widget=forms.Select( attrs={'filter-for': 'vlan', 'nullable': 'true'} ) ) vlan = ChainedModelChoiceField( - queryset=VLAN.objects.all(), chains={'site': 'site'}, required=False, label='VLAN', widget=APISelect( + queryset=VLAN.objects.all(), + chains=( + ('site', 'site'), + ), + required=False, + label='VLAN', + widget=APISelect( api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name' ) ) @@ -322,7 +331,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) ) interface_rack = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'interface_site'}, + chains=( + ('site', 'interface_site'), + ), required=False, label='Rack', widget=APISelect( @@ -333,7 +344,10 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) ) interface_device = ChainedModelChoiceField( queryset=Device.objects.all(), - chains={'site': 'interface_site', 'rack': 'interface_rack'}, + chains=( + ('site', 'interface_site'), + ('rack', 'interface_rack'), + ), required=False, label='Device', widget=APISelect( @@ -344,7 +358,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) ) interface = ChainedModelChoiceField( queryset=Interface.objects.all(), - chains={'device': 'interface_device'}, + chains=( + ('device', 'interface_device'), + ), required=False, widget=APISelect( api_url='/api/dcim/interfaces/?device_id={{interface_device}}' @@ -355,34 +371,41 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) required=False, label='Site', widget=forms.Select( - attrs={'filter-for': 'nat_device'} + attrs={'filter-for': 'nat_rack'} ) ) nat_rack = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'nat_site'}, + chains=( + ('site', 'nat_site'), + ), required=False, label='Rack', widget=APISelect( - api_url='/api/dcim/racks/?site_id={{interface_site}}', + api_url='/api/dcim/racks/?site_id={{nat_site}}', display_field='display_name', attrs={'filter-for': 'nat_device', 'nullable': 'true'} ) ) nat_device = ChainedModelChoiceField( queryset=Device.objects.all(), - chains={'site': 'nat_site'}, + chains=( + ('site', 'nat_site'), + ('rack', 'nat_rack'), + ), required=False, label='Device', widget=APISelect( - api_url='/api/dcim/devices/?site_id={{nat_site}}', + api_url='/api/dcim/devices/?site_id={{nat_site}}&rack_id={{nat_rack}}', display_field='display_name', attrs={'filter-for': 'nat_inside'} ) ) nat_inside = ChainedModelChoiceField( queryset=IPAddress.objects.all(), - chains={'interface__device': 'nat_device'}, + chains=( + ('interface__device', 'nat_device'), + ), required=False, label='IP Address', widget=APISelect( @@ -392,7 +415,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) ) livesearch = forms.CharField( required=False, - label='IP Address', + label='Search', widget=Livesearch( query_key='q', query_url='ipam-api:ipaddress-list', @@ -405,8 +428,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) class Meta: model = IPAddress fields = [ - 'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside', 'tenant_group', - 'tenant', + 'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack', + 'nat_inside', 'tenant_group', 'tenant', ] def __init__(self, *args, **kwargs): @@ -627,7 +650,9 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm): ) group = ChainedModelChoiceField( queryset=VLANGroup.objects.all(), - chains={'site': 'site'}, + chains=( + ('site', 'site'), + ), required=False, label='Group', widget=APISelect( diff --git a/netbox/templates/circuits/circuittermination_edit.html b/netbox/templates/circuits/circuittermination_edit.html index 22e02c4ee..13aa7e5b6 100644 --- a/netbox/templates/circuits/circuittermination_edit.html +++ b/netbox/templates/circuits/circuittermination_edit.html @@ -45,23 +45,8 @@
{% render_field form.site %} -
-
- -
-
-
-
- {% render_field form.rack %} - {% render_field form.device %} -
- -
+ {% render_field form.rack %} + {% render_field form.device %} {% render_field form.interface %} diff --git a/netbox/templates/dcim/consoleport_connect.html b/netbox/templates/dcim/consoleport_connect.html index a04a4674b..e06bf45ec 100644 --- a/netbox/templates/dcim/consoleport_connect.html +++ b/netbox/templates/dcim/consoleport_connect.html @@ -32,12 +32,7 @@ {% render_field form.livesearch %}
-
- -
-

{{ consoleport.device.site }}

-
-
+ {% render_field form.site %} {% render_field form.rack %} {% render_field form.console_server %}
diff --git a/netbox/templates/dcim/consoleserverport_connect.html b/netbox/templates/dcim/consoleserverport_connect.html index 6ba944b59..82b80e3f7 100644 --- a/netbox/templates/dcim/consoleserverport_connect.html +++ b/netbox/templates/dcim/consoleserverport_connect.html @@ -32,12 +32,7 @@ {% render_field form.livesearch %}
-
- -
-

{{ consoleserverport.device.site }}

-
-
+ {% render_field form.site %} {% render_field form.rack %} {% render_field form.device %}
diff --git a/netbox/templates/dcim/poweroutlet_connect.html b/netbox/templates/dcim/poweroutlet_connect.html index 6fcc3e858..839027db2 100644 --- a/netbox/templates/dcim/poweroutlet_connect.html +++ b/netbox/templates/dcim/poweroutlet_connect.html @@ -32,12 +32,7 @@ {% render_field form.livesearch %}
-
- -
-

{{ poweroutlet.device.site }}

-
-
+ {% render_field form.site %} {% render_field form.rack %} {% render_field form.device %}
diff --git a/netbox/templates/dcim/powerport_connect.html b/netbox/templates/dcim/powerport_connect.html index f77a0e352..a10fecc7b 100644 --- a/netbox/templates/dcim/powerport_connect.html +++ b/netbox/templates/dcim/powerport_connect.html @@ -32,12 +32,7 @@ {% render_field form.livesearch %}
-
- -
-

{{ powerport.device.site }}

-
-
+ {% render_field form.site %} {% render_field form.rack %} {% render_field form.pdu %}
diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html index d7aef0fe4..64dc22353 100644 --- a/netbox/templates/ipam/ipaddress_edit.html +++ b/netbox/templates/ipam/ipaddress_edit.html @@ -47,6 +47,7 @@
{% render_field form.nat_site %} + {% render_field form.nat_rack %} {% render_field form.nat_device %}