diff --git a/CHANGELOG.md b/CHANGELOG.md index c2c1984bd..705736d30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -213,6 +213,33 @@ functionality provided by the front end UI. --- +2.5.13 (2019-05-31) + +## Enhancements + +* [#2813](https://github.com/digitalocean/netbox/issues/2813) - Add tenant group filters +* [#3085](https://github.com/digitalocean/netbox/issues/3085) - Catch all exceptions during export template rendering +* [#3138](https://github.com/digitalocean/netbox/issues/3138) - Add 2.5GE and 5GE interface form factors +* [#3151](https://github.com/digitalocean/netbox/issues/3151) - Add inventory item count to manufacturers list +* [#3156](https://github.com/digitalocean/netbox/issues/3156) - Add site link to rack reservations overview +* [#3183](https://github.com/digitalocean/netbox/issues/3183) - Enable bulk deletion of sites +* [#3185](https://github.com/digitalocean/netbox/issues/3185) - Improve performance for custom field access within templates +* [#3186](https://github.com/digitalocean/netbox/issues/3186) - Add interface name filter for IP addresses + +## Bug Fixes + +* [#3031](https://github.com/digitalocean/netbox/issues/3031) - Fixed form field population of tags with spaces +* [#3132](https://github.com/digitalocean/netbox/issues/3132) - Circuit termination missing from available cable termination types +* [#3150](https://github.com/digitalocean/netbox/issues/3150) - Fix formatting of cable length during cable trace +* [#3184](https://github.com/digitalocean/netbox/issues/3184) - Correctly display color block for white cables +* [#3190](https://github.com/digitalocean/netbox/issues/3190) - Fix custom field rendering for Jinja2 export templates +* [#3211](https://github.com/digitalocean/netbox/issues/3211) - Fix error handling when attempting to delete a protected object via API +* [#3223](https://github.com/digitalocean/netbox/issues/3223) - Fix filtering devices by "has power outlets" +* [#3227](https://github.com/digitalocean/netbox/issues/3227) - Fix exception when deleting a circuit with a termination(s) +* [#3228](https://github.com/digitalocean/netbox/issues/3228) - Fixed login link retaining query parameters + +--- + 2.5.12 (2019-05-01) ## Bug Fixes diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 02e95019a..4323feafc 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -3,7 +3,7 @@ from django.db.models import Q from dcim.models import Site from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant +from tenancy.filtersets import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from .constants import CIRCUIT_STATUS_CHOICES from .models import Provider, Circuit, CircuitTermination, CircuitType @@ -54,7 +54,7 @@ class CircuitTypeFilter(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug'] -class CircuitFilter(CustomFieldFilterSet): +class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -87,16 +87,6 @@ class CircuitFilter(CustomFieldFilterSet): choices=CIRCUIT_STATUS_CHOICES, null_value=None ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) site_id = django_filters.ModelMultipleChoiceFilter( field_name='terminations__site', queryset=Site.objects.all(), diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 4deee57c9..100c6334f 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -4,6 +4,7 @@ from taggit.forms import TagField from dcim.models import Site from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from tenancy.forms import TenancyForm +from tenancy.forms import TenancyFilterForm from tenancy.models import Tenant from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, @@ -265,8 +266,9 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit ] -class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): +class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Circuit + field_order = ['q', 'type', 'provider', 'status', 'site', 'tenant_group', 'tenant', 'commit_rate'] q = forms.CharField( required=False, label='Search' @@ -292,16 +294,6 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, widget=StaticSelect2Multiple() ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 5824ad65f..0297790b5 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -274,11 +274,16 @@ class CircuitTermination(CableTermination): """ Reference the parent circuit when recording the change. """ + try: + related_object = self.circuit + except Circuit.DoesNotExist: + # Parent circuit has been deleted + related_object = None ObjectChange( user=user, request_id=request_id, changed_object=self, - related_object=self.circuit, + related_object=related_object, action=action, object_data=serialize_object(self) ).save() diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index 5b818a945..c142a831a 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from dcim.views import CableCreateView, CableTraceView from extras.views import ObjectChangeLogView @@ -9,41 +9,42 @@ app_name = 'circuits' urlpatterns = [ # Providers - url(r'^providers/$', views.ProviderListView.as_view(), name='provider_list'), - url(r'^providers/add/$', views.ProviderCreateView.as_view(), name='provider_add'), - 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.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'), - url(r'^providers/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), + path(r'providers/', views.ProviderListView.as_view(), name='provider_list'), + path(r'providers/add/', views.ProviderCreateView.as_view(), name='provider_add'), + path(r'providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'), + path(r'providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), + path(r'providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), + path(r'providers//', views.ProviderView.as_view(), name='provider'), + path(r'providers//edit/', views.ProviderEditView.as_view(), name='provider_edit'), + path(r'providers//delete/', views.ProviderDeleteView.as_view(), name='provider_delete'), + path(r'providers//changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), # Circuit types - url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'), - url(r'^circuit-types/add/$', views.CircuitTypeCreateView.as_view(), name='circuittype_add'), - url(r'^circuit-types/import/$', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'), - url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'), - url(r'^circuit-types/(?P[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), - url(r'^circuit-types/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), + path(r'circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'), + path(r'circuit-types/add/', views.CircuitTypeCreateView.as_view(), name='circuittype_add'), + path(r'circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'), + path(r'circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'), + path(r'circuit-types//edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), + path(r'circuit-types//changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), # Circuits - url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'), - url(r'^circuits/add/$', views.CircuitCreateView.as_view(), name='circuit_add'), - 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.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+)/changelog/$', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}), - url(r'^circuits/(?P\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'), + path(r'circuits/', views.CircuitListView.as_view(), name='circuit_list'), + path(r'circuits/add/', views.CircuitCreateView.as_view(), name='circuit_add'), + path(r'circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'), + path(r'circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'), + path(r'circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'), + path(r'circuits//', views.CircuitView.as_view(), name='circuit'), + path(r'circuits//edit/', views.CircuitEditView.as_view(), name='circuit_edit'), + path(r'circuits//delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'), + path(r'circuits//changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}), + path(r'circuits//terminations/swap/', views.circuit_terminations_swap, name='circuit_terminations_swap'), # Circuit terminations - url(r'^circuits/(?P\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'), - url(r'^circuit-terminations/(?P\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), - url(r'^circuit-terminations/(?P\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), - url(r'^circuit-terminations/(?P\d+)/connect/(?P[\w-]+)/$', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}), - url(r'^circuit-terminations/(?P\d+)/trace/$', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}), + + path(r'circuits//terminations/add/', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'), + path(r'circuit-terminations//edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), + path(r'circuit-terminations//delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), + path(r'circuit-terminations//connect//', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}), + path(r'circuit-terminations//trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}), ] diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index d55c07660..8ffc249bd 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -75,6 +75,8 @@ IFACE_TYPE_100ME_FIXED = 800 IFACE_TYPE_1GE_FIXED = 1000 IFACE_TYPE_1GE_GBIC = 1050 IFACE_TYPE_1GE_SFP = 1100 +IFACE_TYPE_2GE_FIXED = 1120 +IFACE_TYPE_5GE_FIXED = 1130 IFACE_TYPE_10GE_FIXED = 1150 IFACE_TYPE_10GE_CX4 = 1170 IFACE_TYPE_10GE_SFP_PLUS = 1200 @@ -150,6 +152,8 @@ IFACE_TYPE_CHOICES = [ [ [IFACE_TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'], [IFACE_TYPE_1GE_FIXED, '1000BASE-T (1GE)'], + [IFACE_TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'], + [IFACE_TYPE_5GE_FIXED, '5GBASE-T (5GE)'], [IFACE_TYPE_10GE_FIXED, '10GBASE-T (10GE)'], [IFACE_TYPE_10GE_CX4, '10GBASE-CX4 (10GE)'], ] @@ -360,7 +364,7 @@ CONNECTION_STATUS_CHOICES = [ # Cable endpoint types CABLE_TERMINATION_TYPES = [ - 'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', + 'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination', ] # Cable types diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 48c38dd30..a6c038fae 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -6,6 +6,7 @@ from netaddr import EUI from netaddr.core import AddrFormatError from extras.filters import CustomFieldFilterSet +from tenancy.filtersets import TenancyFilterSet from tenancy.models import Tenant from utilities.constants import COLOR_CHOICES from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter @@ -37,7 +38,7 @@ class RegionFilter(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug'] -class SiteFilter(CustomFieldFilterSet): +class SiteFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -61,16 +62,6 @@ class SiteFilter(CustomFieldFilterSet): to_field_name='slug', label='Region (slug)', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) tag = TagFilter() class Meta: @@ -125,7 +116,7 @@ class RackRoleFilter(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug', 'color'] -class RackFilter(CustomFieldFilterSet): +class RackFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -154,16 +145,6 @@ class RackFilter(CustomFieldFilterSet): to_field_name='slug', label='Group', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) status = django_filters.MultipleChoiceFilter( choices=RACK_STATUS_CHOICES, null_value=None @@ -199,7 +180,7 @@ class RackFilter(CustomFieldFilterSet): ) -class RackReservationFilter(django_filters.FilterSet): +class RackReservationFilter(TenancyFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -234,16 +215,6 @@ class RackReservationFilter(django_filters.FilterSet): to_field_name='slug', label='Group', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) user_id = django_filters.ModelMultipleChoiceFilter( queryset=User.objects.all(), label='User (ID)', @@ -449,7 +420,7 @@ class PlatformFilter(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug', 'napalm_driver'] -class DeviceFilter(CustomFieldFilterSet): +class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -484,16 +455,6 @@ class DeviceFilter(CustomFieldFilterSet): to_field_name='slug', label='Role (slug)', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) platform_id = django_filters.ModelMultipleChoiceFilter( queryset=Platform.objects.all(), label='Platform (ID)', @@ -639,7 +600,7 @@ class DeviceFilter(CustomFieldFilterSet): return queryset.exclude(powerports__isnull=value) def _power_outlets(self, queryset, name, value): - return queryset.exclude(poweroutlets_isnull=value) + return queryset.exclude(poweroutlets__isnull=value) def _interfaces(self, queryset, name, value): return queryset.exclude(interfaces__isnull=value) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 1586f2d6c..e946e18c2 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -14,7 +14,8 @@ from circuits.models import Circuit, Provider from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from ipam.models import IPAddress, VLAN, VLANGroup from tenancy.forms import TenancyForm -from tenancy.models import Tenant +from tenancy.forms import TenancyFilterForm +from tenancy.models import Tenant, TenantGroup from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm, @@ -256,8 +257,9 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor ] -class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): +class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Site + field_order = ['q', 'status', 'region', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' @@ -276,16 +278,6 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): value_field="slug", ) ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) # @@ -596,8 +588,9 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor ] -class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): +class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Rack + field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' @@ -619,16 +612,6 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) status = forms.MultipleChoiceField( choices=RACK_STATUS_CHOICES, required=False, @@ -689,40 +672,6 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm): return unit_choices -class RackReservationFilterForm(BootstrapMixin, forms.Form): - q = forms.CharField( - required=False, - label='Search' - ) - site = FilterChoiceField( - queryset=Site.objects.all(), - to_field_name='slug', - widget=APISelectMultiple( - api_url="/api/dcim/sites/", - value_field="slug", - ) - ) - group_id = FilterChoiceField( - queryset=RackGroup.objects.select_related('site'), - label='Rack group', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/dcim/rack-groups/", - null_option=True, - ) - ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) - - class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RackReservation.objects.all(), @@ -751,6 +700,31 @@ class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): nullable_fields = [] +class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm): + field_order = ['q', 'site', 'group_id', 'tenant_group', 'tenant'] + q = forms.CharField( + required=False, + label='Search' + ) + site = FilterChoiceField( + queryset=Site.objects.all(), + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field="slug", + ) + ) + group_id = FilterChoiceField( + queryset=RackGroup.objects.select_related('site'), + label='Rack group', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/dcim/rack-groups/", + null_option=True, + ) + ) + + # # Manufacturers # @@ -1656,8 +1630,12 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF ] -class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): +class DeviceFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Device + field_order = [ + 'q', 'region', 'site', 'rack_group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant', + 'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip', + ] q = forms.CharField( required=False, label='Search' @@ -1715,16 +1693,6 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): value_field="slug", ) ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) manufacturer_id = FilterChoiceField( queryset=Manufacturer.objects.all(), label='Manufacturer', @@ -3368,11 +3336,33 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field="slug", + ) + ) + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + null_option=True, + filter_for={ + 'tenant': 'group' + } + ) ) tenant = FilterChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenants/", + value_field="slug", + null_option=True, + ) ) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index c9bf8ac00..55afb224e 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -314,7 +314,12 @@ class RackDetailTable(RackTable): class RackReservationTable(BaseTable): pk = ToggleColumn() - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + site = tables.LinkColumn( + viewname='dcim:site', + accessor=Accessor('rack.site'), + args=[Accessor('rack.site.slug')], + ) + tenant = tables.TemplateColumn(template_code=COL_TENANT) rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) unit_list = tables.Column(orderable=False, verbose_name='Units') actions = tables.TemplateColumn( @@ -323,7 +328,7 @@ class RackReservationTable(BaseTable): class Meta(BaseTable.Meta): model = RackReservation - fields = ('pk', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions') + fields = ('pk', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions') # @@ -332,16 +337,26 @@ class RackReservationTable(BaseTable): class ManufacturerTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(verbose_name='Name') - devicetype_count = tables.Column(verbose_name='Device Types') - platform_count = tables.Column(verbose_name='Platforms') - slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, - verbose_name='') + name = tables.LinkColumn() + devicetype_count = tables.Column( + verbose_name='Device Types' + ) + inventoryitem_count = tables.Column( + verbose_name='Inventory Items' + ) + platform_count = tables.Column( + verbose_name='Platforms' + ) + slug = tables.Column() + actions = tables.TemplateColumn( + template_code=MANUFACTURER_ACTIONS, + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = Manufacturer - fields = ('pk', 'name', 'devicetype_count', 'platform_count', 'slug', 'actions') + fields = ('pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'slug', 'actions') # diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 01a8c4e42..ae1f05757 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from extras.views import ObjectChangeLogView, ImageAttachmentEditView from ipam.views import ServiceCreateView @@ -14,293 +14,294 @@ app_name = 'dcim' urlpatterns = [ # Regions - url(r'^regions/$', views.RegionListView.as_view(), name='region_list'), - url(r'^regions/add/$', views.RegionCreateView.as_view(), name='region_add'), - url(r'^regions/import/$', views.RegionBulkImportView.as_view(), name='region_import'), - url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'), - url(r'^regions/(?P\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'), - url(r'^regions/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}), + path(r'regions/', views.RegionListView.as_view(), name='region_list'), + path(r'regions/add/', views.RegionCreateView.as_view(), name='region_add'), + path(r'regions/import/', views.RegionBulkImportView.as_view(), name='region_import'), + path(r'regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'), + path(r'regions//edit/', views.RegionEditView.as_view(), name='region_edit'), + path(r'regions//changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}), # Sites - url(r'^sites/$', views.SiteListView.as_view(), name='site_list'), - url(r'^sites/add/$', views.SiteCreateView.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.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[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}), - url(r'^sites/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}), + path(r'sites/', views.SiteListView.as_view(), name='site_list'), + path(r'sites/add/', views.SiteCreateView.as_view(), name='site_add'), + path(r'sites/import/', views.SiteBulkImportView.as_view(), name='site_import'), + path(r'sites/edit/', views.SiteBulkEditView.as_view(), name='site_bulk_edit'), + path(r'sites/delete/', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'), + path(r'sites//', views.SiteView.as_view(), name='site'), + path(r'sites//edit/', views.SiteEditView.as_view(), name='site_edit'), + path(r'sites//delete/', views.SiteDeleteView.as_view(), name='site_delete'), + path(r'sites//changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}), + path(r'sites//images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}), # Rack groups - url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'), - url(r'^rack-groups/add/$', views.RackGroupCreateView.as_view(), name='rackgroup_add'), - url(r'^rack-groups/import/$', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'), - url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'), - url(r'^rack-groups/(?P\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'), - url(r'^rack-groups/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}), + path(r'rack-groups/', views.RackGroupListView.as_view(), name='rackgroup_list'), + path(r'rack-groups/add/', views.RackGroupCreateView.as_view(), name='rackgroup_add'), + path(r'rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'), + path(r'rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'), + path(r'rack-groups//edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'), + path(r'rack-groups//changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}), # Rack roles - url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'), - url(r'^rack-roles/add/$', views.RackRoleCreateView.as_view(), name='rackrole_add'), - url(r'^rack-roles/import/$', views.RackRoleBulkImportView.as_view(), name='rackrole_import'), - url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'), - url(r'^rack-roles/(?P\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'), - url(r'^rack-roles/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}), + path(r'rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'), + path(r'rack-roles/add/', views.RackRoleCreateView.as_view(), name='rackrole_add'), + path(r'rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'), + path(r'rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'), + path(r'rack-roles//edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'), + path(r'rack-roles//changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}), # Rack reservations - url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'), - url(r'^rack-reservations/edit/$', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'), - url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), - url(r'^rack-reservations/(?P\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'), - url(r'^rack-reservations/(?P\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), - url(r'^rack-reservations/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}), + path(r'rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'), + path(r'rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'), + path(r'rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), + path(r'rack-reservations//edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'), + path(r'rack-reservations//delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), + path(r'rack-reservations//changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}), # Racks - url(r'^racks/$', views.RackListView.as_view(), name='rack_list'), - url(r'^rack-elevations/$', views.RackElevationListView.as_view(), name='rack_elevation_list'), - url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'), - 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.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+)/changelog/$', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}), - url(r'^racks/(?P\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'), - url(r'^racks/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}), + path(r'racks/', views.RackListView.as_view(), name='rack_list'), + path(r'rack-elevations/', views.RackElevationListView.as_view(), name='rack_elevation_list'), + path(r'racks/add/', views.RackEditView.as_view(), name='rack_add'), + path(r'racks/import/', views.RackBulkImportView.as_view(), name='rack_import'), + path(r'racks/edit/', views.RackBulkEditView.as_view(), name='rack_bulk_edit'), + path(r'racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'), + path(r'racks//', views.RackView.as_view(), name='rack'), + path(r'racks//edit/', views.RackEditView.as_view(), name='rack_edit'), + path(r'racks//delete/', views.RackDeleteView.as_view(), name='rack_delete'), + path(r'racks//changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}), + path(r'racks//reservations/add/', views.RackReservationCreateView.as_view(), name='rack_add_reservation'), + path(r'racks//images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}), # Manufacturers - url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'), - url(r'^manufacturers/add/$', views.ManufacturerCreateView.as_view(), name='manufacturer_add'), - url(r'^manufacturers/import/$', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'), - url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'), - url(r'^manufacturers/(?P[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), - url(r'^manufacturers/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}), + path(r'manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'), + path(r'manufacturers/add/', views.ManufacturerCreateView.as_view(), name='manufacturer_add'), + path(r'manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'), + path(r'manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'), + path(r'manufacturers//edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), + path(r'manufacturers//changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}), # Device types - url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'), - url(r'^device-types/add/$', views.DeviceTypeCreateView.as_view(), name='devicetype_add'), - url(r'^device-types/import/$', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'), - 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.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'), - url(r'^device-types/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), + path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'), + path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'), + path(r'device-types/import/', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'), + path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), + path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), + path(r'device-types//', views.DeviceTypeView.as_view(), name='devicetype'), + path(r'device-types//edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), + path(r'device-types//delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), + path(r'device-types//changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), # Console port templates - url(r'^device-types/(?P\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'), - url(r'^device-types/(?P\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'), + path(r'device-types//console-ports/add/', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'), + path(r'device-types//console-ports/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'), # Console server port templates - url(r'^device-types/(?P\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'), - url(r'^device-types/(?P\d+)/console-server-ports/delete/$', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'), + path(r'device-types//console-server-ports/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'), + path(r'device-types//console-server-ports/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'), # Power port templates - url(r'^device-types/(?P\d+)/power-ports/add/$', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'), - url(r'^device-types/(?P\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'), + path(r'device-types//power-ports/add/', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'), + path(r'device-types//power-ports/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'), # Power outlet templates - url(r'^device-types/(?P\d+)/power-outlets/add/$', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'), - url(r'^device-types/(?P\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'), + path(r'device-types//power-outlets/add/', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'), + path(r'device-types//power-outlets/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'), # Interface templates - url(r'^device-types/(?P\d+)/interfaces/add/$', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'), - url(r'^device-types/(?P\d+)/interfaces/edit/$', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'), - url(r'^device-types/(?P\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'), + path(r'device-types//interfaces/add/', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'), + path(r'device-types//interfaces/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'), + path(r'device-types//interfaces/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'), # Front port templates - url(r'^device-types/(?P\d+)/front-ports/add/$', views.FrontPortTemplateCreateView.as_view(), name='devicetype_add_frontport'), - url(r'^device-types/(?P\d+)/front-ports/delete/$', views.FrontPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_frontport'), + path(r'device-types//front-ports/add/', views.FrontPortTemplateCreateView.as_view(), name='devicetype_add_frontport'), + path(r'device-types//front-ports/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_frontport'), # Rear port templates - url(r'^device-types/(?P\d+)/rear-ports/add/$', views.RearPortTemplateCreateView.as_view(), name='devicetype_add_rearport'), - url(r'^device-types/(?P\d+)/rear-ports/delete/$', views.RearPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_rearport'), + path(r'device-types//rear-ports/add/', views.RearPortTemplateCreateView.as_view(), name='devicetype_add_rearport'), + path(r'device-types//rear-ports/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_rearport'), # Device bay templates - url(r'^device-types/(?P\d+)/device-bays/add/$', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'), - url(r'^device-types/(?P\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'), + path(r'device-types//device-bays/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'), + path(r'device-types//device-bays/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'), # Device roles - url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'), - url(r'^device-roles/add/$', views.DeviceRoleCreateView.as_view(), name='devicerole_add'), - url(r'^device-roles/import/$', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'), - url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'), - url(r'^device-roles/(?P[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), - url(r'^device-roles/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}), + path(r'device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'), + path(r'device-roles/add/', views.DeviceRoleCreateView.as_view(), name='devicerole_add'), + path(r'device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'), + path(r'device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'), + path(r'device-roles//edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), + path(r'device-roles//changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}), # Platforms - url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'), - url(r'^platforms/add/$', views.PlatformCreateView.as_view(), name='platform_add'), - url(r'^platforms/import/$', views.PlatformBulkImportView.as_view(), name='platform_import'), - url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'), - url(r'^platforms/(?P[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'), - url(r'^platforms/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}), + path(r'platforms/', views.PlatformListView.as_view(), name='platform_list'), + path(r'platforms/add/', views.PlatformCreateView.as_view(), name='platform_add'), + path(r'platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'), + path(r'platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'), + path(r'platforms//edit/', views.PlatformEditView.as_view(), name='platform_edit'), + path(r'platforms//changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}), # Devices - url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'), - url(r'^devices/add/$', views.DeviceCreateView.as_view(), name='device_add'), - url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'), - 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.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+)/config-context/$', views.DeviceConfigContextView.as_view(), name='device_configcontext'), - url(r'^devices/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}), - url(r'^devices/(?P\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'), - url(r'^devices/(?P\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'), - url(r'^devices/(?P\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), - url(r'^devices/(?P\d+)/config/$', views.DeviceConfigView.as_view(), name='device_config'), - url(r'^devices/(?P\d+)/add-secret/$', secret_add, name='device_addsecret'), - url(r'^devices/(?P\d+)/services/assign/$', ServiceCreateView.as_view(), name='device_service_assign'), - url(r'^devices/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), + path(r'devices/', views.DeviceListView.as_view(), name='device_list'), + path(r'devices/add/', views.DeviceCreateView.as_view(), name='device_add'), + path(r'devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'), + path(r'devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'), + path(r'devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'), + path(r'devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'), + path(r'devices//', views.DeviceView.as_view(), name='device'), + path(r'devices//edit/', views.DeviceEditView.as_view(), name='device_edit'), + path(r'devices//delete/', views.DeviceDeleteView.as_view(), name='device_delete'), + path(r'devices//config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'), + path(r'devices//changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}), + path(r'devices//inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'), + path(r'devices//status/', views.DeviceStatusView.as_view(), name='device_status'), + path(r'devices//lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), + path(r'devices//config/', views.DeviceConfigView.as_view(), name='device_config'), + path(r'devices//add-secret/', secret_add, name='device_addsecret'), + path(r'devices//services/assign/', ServiceCreateView.as_view(), name='device_service_assign'), + path(r'devices//images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), # Console ports - url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), - url(r'^devices/(?P\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'), - url(r'^devices/(?P\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'), - url(r'^console-ports/(?P\d+)/connect/(?P[\w-]+)/$', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}), - url(r'^console-ports/(?P\d+)/edit/$', views.ConsolePortEditView.as_view(), name='consoleport_edit'), - url(r'^console-ports/(?P\d+)/delete/$', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), - url(r'^console-ports/(?P\d+)/trace/$', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), + path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), + path(r'devices//console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'), + path(r'devices//console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'), + path(r'console-ports//connect//', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}), + path(r'console-ports//edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), + path(r'console-ports//delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), + path(r'console-ports//trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), # Console server ports - url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), - url(r'^devices/(?P\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'), - url(r'^devices/(?P\d+)/console-server-ports/edit/$', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'), - url(r'^devices/(?P\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'), - url(r'^console-server-ports/(?P\d+)/connect/(?P[\w-]+)/$', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}), - url(r'^console-server-ports/(?P\d+)/edit/$', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), - url(r'^console-server-ports/(?P\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), - url(r'^console-server-ports/(?P\d+)/trace/$', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), - url(r'^console-server-ports/rename/$', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'), - url(r'^console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'), + path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), + path(r'devices//console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'), + path(r'devices//console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'), + path(r'devices//console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'), + path(r'console-server-ports//connect//', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}), + path(r'console-server-ports//edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), + path(r'console-server-ports//delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), + path(r'console-server-ports//trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), + path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'), + path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'), # Power ports - url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), - url(r'^devices/(?P\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'), - url(r'^devices/(?P\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'), - url(r'^power-ports/(?P\d+)/connect/(?P[\w-]+)/$', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}), - url(r'^power-ports/(?P\d+)/edit/$', views.PowerPortEditView.as_view(), name='powerport_edit'), - url(r'^power-ports/(?P\d+)/delete/$', views.PowerPortDeleteView.as_view(), name='powerport_delete'), - url(r'^power-ports/(?P\d+)/trace/$', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), + path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), + path(r'devices//power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'), + path(r'devices//power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'), + path(r'power-ports//connect//', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}), + path(r'power-ports//edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), + path(r'power-ports//delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), + path(r'power-ports//trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), # Power outlets - url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), - url(r'^devices/(?P\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'), - url(r'^devices/(?P\d+)/power-outlets/edit/$', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'), - url(r'^devices/(?P\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), - url(r'^power-outlets/(?P\d+)/connect/(?P[\w-]+)/$', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}), - url(r'^power-outlets/(?P\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), - url(r'^power-outlets/(?P\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), - url(r'^power-outlets/(?P\d+)/trace/$', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), - url(r'^power-outlets/rename/$', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'), - url(r'^power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'), + path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), + path(r'devices//power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'), + path(r'devices//power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'), + path(r'devices//power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), + path(r'power-outlets//connect//', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}), + path(r'power-outlets//edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), + path(r'power-outlets//delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), + path(r'power-outlets//trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), + path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'), + path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'), # Interfaces - url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), - url(r'^devices/(?P\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'), - url(r'^devices/(?P\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), - url(r'^devices/(?P\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), - url(r'^interfaces/(?P\d+)/connect/(?P[\w-]+)/$', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), - url(r'^interfaces/(?P\d+)/$', views.InterfaceView.as_view(), name='interface'), - url(r'^interfaces/(?P\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'), - url(r'^interfaces/(?P\d+)/assign-vlans/$', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'), - url(r'^interfaces/(?P\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'), - url(r'^interfaces/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}), - url(r'^interfaces/(?P\d+)/trace/$', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), - url(r'^interfaces/rename/$', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'), - url(r'^interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), + path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), + path(r'devices//interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'), + path(r'devices//interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), + path(r'devices//interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), + path(r'interfaces//connect//', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), + path(r'interfaces//', views.InterfaceView.as_view(), name='interface'), + path(r'interfaces//edit/', views.InterfaceEditView.as_view(), name='interface_edit'), + path(r'interfaces//assign-vlans/', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'), + path(r'interfaces//delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), + path(r'interfaces//changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}), + path(r'interfaces//trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), + path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'), + path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), # Front ports - # url(r'^devices/front-ports/add/$', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), - url(r'^devices/(?P\d+)/front-ports/add/$', views.FrontPortCreateView.as_view(), name='frontport_add'), - url(r'^devices/(?P\d+)/front-ports/edit/$', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'), - url(r'^devices/(?P\d+)/front-ports/delete/$', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'), - url(r'^front-ports/(?P\d+)/connect/(?P[\w-]+)/$', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), - url(r'^front-ports/(?P\d+)/edit/$', views.FrontPortEditView.as_view(), name='frontport_edit'), - url(r'^front-ports/(?P\d+)/delete/$', views.FrontPortDeleteView.as_view(), name='frontport_delete'), - url(r'^front-ports/(?P\d+)/trace/$', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), - url(r'^front-ports/rename/$', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'), - url(r'^front-ports/disconnect/$', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'), + # path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), + path(r'devices//front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'), + path(r'devices//front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'), + path(r'devices//front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'), + path(r'front-ports//connect//', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), + path(r'front-ports//edit/', views.FrontPortEditView.as_view(), name='frontport_edit'), + path(r'front-ports//delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), + path(r'front-ports//trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), + path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'), + path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'), # Rear ports - # url(r'^devices/rear-ports/add/$', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), - url(r'^devices/(?P\d+)/rear-ports/add/$', views.RearPortCreateView.as_view(), name='rearport_add'), - url(r'^devices/(?P\d+)/rear-ports/edit/$', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'), - url(r'^devices/(?P\d+)/rear-ports/delete/$', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'), - url(r'^rear-ports/(?P\d+)/connect/(?P[\w-]+)/$', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), - url(r'^rear-ports/(?P\d+)/edit/$', views.RearPortEditView.as_view(), name='rearport_edit'), - url(r'^rear-ports/(?P\d+)/delete/$', views.RearPortDeleteView.as_view(), name='rearport_delete'), - url(r'^rear-ports/(?P\d+)/trace/$', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), - url(r'^rear-ports/rename/$', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'), - url(r'^rear-ports/disconnect/$', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'), + # path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), + path(r'devices//rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'), + path(r'devices//rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'), + path(r'devices//rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'), + path(r'rear-ports//connect//', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), + path(r'rear-ports//edit/', views.RearPortEditView.as_view(), name='rearport_edit'), + path(r'rear-ports//delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), + path(r'rear-ports//trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), + path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'), + path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'), # Device bays - url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), - url(r'^devices/(?P\d+)/bays/add/$', views.DeviceBayCreateView.as_view(), name='devicebay_add'), - url(r'^devices/(?P\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'), - url(r'^device-bays/(?P\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'), - url(r'^device-bays/(?P\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), - url(r'^device-bays/(?P\d+)/populate/$', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), - url(r'^device-bays/(?P\d+)/depopulate/$', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'), - url(r'^device-bays/rename/$', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'), + path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), + path(r'devices//bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'), + path(r'devices//bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'), + path(r'device-bays//edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'), + path(r'device-bays//delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), + path(r'device-bays//populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), + path(r'device-bays//depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'), + path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'), # Inventory items - url(r'^inventory-items/$', views.InventoryItemListView.as_view(), name='inventoryitem_list'), - url(r'^inventory-items/import/$', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'), - url(r'^inventory-items/edit/$', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'), - url(r'^inventory-items/delete/$', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'), - url(r'^inventory-items/(?P\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'), - url(r'^inventory-items/(?P\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'), - url(r'^devices/(?P\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'), + path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'), + path(r'inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'), + path(r'inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'), + path(r'inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'), + path(r'inventory-items//edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'), + path(r'inventory-items//delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'), + path(r'devices//inventory-items/add/', views.InventoryItemEditView.as_view(), name='inventoryitem_add'), # Cables - url(r'^cables/$', views.CableListView.as_view(), name='cable_list'), - url(r'^cables/import/$', views.CableBulkImportView.as_view(), name='cable_import'), - url(r'^cables/edit/$', views.CableBulkEditView.as_view(), name='cable_bulk_edit'), - url(r'^cables/delete/$', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'), - url(r'^cables/(?P\d+)/$', views.CableView.as_view(), name='cable'), - url(r'^cables/(?P\d+)/edit/$', views.CableEditView.as_view(), name='cable_edit'), - url(r'^cables/(?P\d+)/delete/$', views.CableDeleteView.as_view(), name='cable_delete'), - url(r'^cables/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}), + path(r'cables/', views.CableListView.as_view(), name='cable_list'), + path(r'cables/import/', views.CableBulkImportView.as_view(), name='cable_import'), + path(r'cables/edit/', views.CableBulkEditView.as_view(), name='cable_bulk_edit'), + path(r'cables/delete/', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'), + path(r'cables//', views.CableView.as_view(), name='cable'), + path(r'cables//edit/', views.CableEditView.as_view(), name='cable_edit'), + path(r'cables//delete/', views.CableDeleteView.as_view(), name='cable_delete'), + path(r'cables//changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}), # Console/power/interface connections (read-only) - url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), - url(r'^power-connections/$', views.PowerConnectionsListView.as_view(), name='power_connections_list'), - url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'), + path(r'console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), + path(r'power-connections/', views.PowerConnectionsListView.as_view(), name='power_connections_list'), + path(r'interface-connections/', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'), # Virtual chassis - url(r'^virtual-chassis/$', views.VirtualChassisListView.as_view(), name='virtualchassis_list'), - url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'), - url(r'^virtual-chassis/(?P\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'), - url(r'^virtual-chassis/(?P\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'), - url(r'^virtual-chassis/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}), - url(r'^virtual-chassis/(?P\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), - url(r'^virtual-chassis-members/(?P\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'), + path(r'virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'), + path(r'virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'), + path(r'virtual-chassis//edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'), + path(r'virtual-chassis//delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'), + path(r'virtual-chassis//changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}), + path(r'virtual-chassis//add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), + path(r'virtual-chassis-members//delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'), # Power panels - url(r'^power-panels/$', views.PowerPanelListView.as_view(), name='powerpanel_list'), - url(r'^power-panels/add/$', views.PowerPanelCreateView.as_view(), name='powerpanel_add'), - url(r'^power-panels/import/$', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'), - url(r'^power-panels/delete/$', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'), - url(r'^power-panels/(?P\d+)/$', views.PowerPanelView.as_view(), name='powerpanel'), - url(r'^power-panels/(?P\d+)/edit/$', views.PowerPanelEditView.as_view(), name='powerpanel_edit'), - url(r'^power-panels/(?P\d+)/delete/$', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'), - url(r'^power-panels/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}), + path(r'power-panels/', views.PowerPanelListView.as_view(), name='powerpanel_list'), + path(r'power-panels/add/', views.PowerPanelCreateView.as_view(), name='powerpanel_add'), + path(r'power-panels/import/', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'), + path(r'power-panels/delete/', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'), + path(r'power-panels//', views.PowerPanelView.as_view(), name='powerpanel'), + path(r'power-panels//edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'), + path(r'power-panels//delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'), + path(r'power-panels//changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}), # Power feeds - url(r'^power-feeds/$', views.PowerFeedListView.as_view(), name='powerfeed_list'), - url(r'^power-feeds/add/$', views.PowerFeedEditView.as_view(), name='powerfeed_add'), - url(r'^power-feeds/import/$', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'), - url(r'^power-feeds/edit/$', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'), - url(r'^power-feeds/delete/$', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'), - url(r'^power-feeds/(?P\d+)/$', views.PowerFeedView.as_view(), name='powerfeed'), - url(r'^power-feeds/(?P\d+)/edit/$', views.PowerFeedEditView.as_view(), name='powerfeed_edit'), - url(r'^power-feeds/(?P\d+)/delete/$', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'), - url(r'^power-feeds/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), + path(r'power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'), + path(r'power-feeds/add/', views.PowerFeedEditView.as_view(), name='powerfeed_add'), + path(r'power-feeds/import/', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'), + path(r'power-feeds/edit/', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'), + path(r'power-feeds/delete/', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'), + path(r'power-feeds//', views.PowerFeedView.as_view(), name='powerfeed'), + path(r'power-feeds//edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'), + path(r'power-feeds//delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'), + path(r'power-feeds//changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), ] diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index d7722dc47..cf152e646 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -253,6 +253,14 @@ class SiteBulkEditView(PermissionRequiredMixin, BulkEditView): default_return_url = 'dcim:site_list' +class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_site' + queryset = Site.objects.select_related('region', 'tenant') + filter = filters.SiteFilter + table = tables.SiteTable + default_return_url = 'dcim:site_list' + + # # Rack groups # @@ -464,7 +472,7 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackReservationListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_rackreservation' - queryset = RackReservation.objects.all() + queryset = RackReservation.objects.select_related('rack__site') filter = filters.RackReservationFilter filter_form = forms.RackReservationFilterForm table = tables.RackReservationTable @@ -523,6 +531,7 @@ class ManufacturerListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_manufacturer' queryset = Manufacturer.objects.annotate( devicetype_count=Count('device_types', distinct=True), + inventoryitem_count=Count('inventory_items', distinct=True), platform_count=Count('platforms', distinct=True), ) table = tables.ManufacturerTable diff --git a/netbox/extras/middleware.py b/netbox/extras/middleware.py index 025aea8df..b0b5a014d 100644 --- a/netbox/extras/middleware.py +++ b/netbox/extras/middleware.py @@ -30,10 +30,6 @@ def cache_changed_object(instance, **kwargs): def _record_object_deleted(request, instance, **kwargs): - # Force resolution of request.user in case it's still a SimpleLazyObject. This seems to happen - # occasionally during tests, but haven't been able to determine why. - assert request.user.is_authenticated - # Record that the object was deleted if hasattr(instance, 'log_change'): instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE) @@ -53,7 +49,7 @@ class ObjectChangeMiddleware(object): 2. Enqueue any relevant webhooks. 3. Increment metric counter for the event type - The post_save and pre_delete signals are employed to catch object modifications, however changes are recorded a bit + The post_save and post_delete signals are employed to catch object modifications, however changes are recorded a bit differently for each. Objects being saved are cached into thread-local storage for action *after* the response has completed. This ensures that serialization of the object is performed only after any related objects (e.g. tags) have been created. Conversely, deletions are acted upon immediately, so that the serialized representation of the @@ -71,10 +67,10 @@ class ObjectChangeMiddleware(object): # the same request. request.id = uuid.uuid4() - # Signals don't include the request context, so we're currying it into the pre_delete function ahead of time. + # Signals don't include the request context, so we're currying it into the post_delete function ahead of time. record_object_deleted = curry(_record_object_deleted, request) - # Connect our receivers to the post_save and pre_delete signals. + # Connect our receivers to the post_save and post_delete signals. post_save.connect(cache_changed_object, dispatch_uid='record_object_saved') post_delete.connect(record_object_deleted, dispatch_uid='record_object_deleted') diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 5994749b7..c5df5c2e5 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -108,17 +108,22 @@ class Webhook(models.Model): # class CustomFieldModel(models.Model): + _cf = None class Meta: abstract = True + @property def cf(self): """ Name-based CustomFieldValue accessor for use in templates """ - if not hasattr(self, 'get_custom_fields'): - return dict() - return {field.name: value for field, value in self.get_custom_fields().items()} + if self._cf is None: + # Cache all custom field values for this instance + self._cf = { + field.name: value for field, value in self.get_custom_fields().items() + } + return self._cf def get_custom_fields(self): """ @@ -131,7 +136,7 @@ class CustomFieldModel(models.Model): # If the object exists, populate its custom fields with values if hasattr(self, 'pk'): - values = CustomFieldValue.objects.filter(obj_type=content_type, obj_id=self.pk).select_related('field') + values = self.custom_field_values.all() values_dict = {cfv.field_id: cfv.value for cfv in values} return OrderedDict([(field, values_dict.get(field.pk)) for field in fields]) else: diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py index 439323c94..70c93968f 100644 --- a/netbox/extras/querysets.py +++ b/netbox/extras/querysets.py @@ -1,6 +1,24 @@ +from collections import OrderedDict + from django.db.models import Q, QuerySet +class CustomFieldQueryset: + """ + Annotate custom fields on objects within a QuerySet. + """ + def __init__(self, queryset, custom_fields): + self.queryset = queryset + self.model = queryset.model + self.custom_fields = custom_fields + + def __iter__(self): + for obj in self.queryset: + values_dict = {cfv.field_id: cfv.value for cfv in obj.custom_field_values.all()} + obj.custom_fields = OrderedDict([(field, values_dict.get(field.pk)) for field in self.custom_fields]) + yield obj + + class ConfigContextQuerySet(QuerySet): def get_for_object(self, obj): diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index d475ab5a3..ad6eabe1e 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from extras import views from extras.models import Tag @@ -8,33 +8,33 @@ app_name = 'extras' urlpatterns = [ # Tags - url(r'^tags/$', views.TagListView.as_view(), name='tag_list'), - url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'), - url(r'^tags/(?P[\w-]+)/$', views.TagView.as_view(), name='tag'), - url(r'^tags/(?P[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'), - url(r'^tags/(?P[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'), - url(r'^tags/(?P[\w-]+)/changelog/$', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}), + path(r'tags/', views.TagListView.as_view(), name='tag_list'), + path(r'tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'), + path(r'tags//', views.TagView.as_view(), name='tag'), + path(r'tags//edit/', views.TagEditView.as_view(), name='tag_edit'), + path(r'tags//delete/', views.TagDeleteView.as_view(), name='tag_delete'), + path(r'tags//changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}), # Config contexts - url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'), - url(r'^config-contexts/add/$', views.ConfigContextCreateView.as_view(), name='configcontext_add'), - url(r'^config-contexts/edit/$', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'), - url(r'^config-contexts/delete/$', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'), - url(r'^config-contexts/(?P\d+)/$', views.ConfigContextView.as_view(), name='configcontext'), - url(r'^config-contexts/(?P\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'), - url(r'^config-contexts/(?P\d+)/delete/$', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'), + path(r'config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'), + path(r'config-contexts/add/', views.ConfigContextCreateView.as_view(), name='configcontext_add'), + path(r'config-contexts/edit/', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'), + path(r'config-contexts/delete/', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'), + path(r'config-contexts//', views.ConfigContextView.as_view(), name='configcontext'), + path(r'config-contexts//edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'), + path(r'config-contexts//delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'), # Image attachments - url(r'^image-attachments/(?P\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'), - url(r'^image-attachments/(?P\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'), + path(r'image-attachments//edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'), + path(r'image-attachments//delete/', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'), # Reports - url(r'^reports/$', views.ReportListView.as_view(), name='report_list'), - url(r'^reports/(?P[^/]+\.[^/]+)/$', views.ReportView.as_view(), name='report'), - url(r'^reports/(?P[^/]+\.[^/]+)/run/$', views.ReportRunView.as_view(), name='report_run'), + path(r'reports/', views.ReportListView.as_view(), name='report_list'), + path(r'reports//', views.ReportView.as_view(), name='report'), + path(r'reports//run/', views.ReportRunView.as_view(), name='report_run'), # Change logging - url(r'^changelog/$', views.ObjectChangeListView.as_view(), name='objectchange_list'), - url(r'^changelog/(?P\d+)/$', views.ObjectChangeView.as_view(), name='objectchange'), + path(r'changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'), + path(r'changelog//', views.ObjectChangeView.as_view(), name='objectchange'), ] diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 5558094eb..49c547d2d 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -6,14 +6,14 @@ from netaddr.core import AddrFormatError from dcim.models import Site, Device, Interface from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant +from tenancy.filtersets import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from virtualization.models import VirtualMachine from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF -class VRFFilter(CustomFieldFilterSet): +class VRFFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -22,16 +22,6 @@ class VRFFilter(CustomFieldFilterSet): method='search', label='Search', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) tag = TagFilter() def search(self, queryset, name, value): @@ -120,7 +110,7 @@ class RoleFilter(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug'] -class PrefixFilter(CustomFieldFilterSet): +class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -159,16 +149,6 @@ class PrefixFilter(CustomFieldFilterSet): to_field_name='rd', label='VRF (RD)', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -267,7 +247,7 @@ class PrefixFilter(CustomFieldFilterSet): return queryset.filter(prefix__net_mask_length=value) -class IPAddressFilter(CustomFieldFilterSet): +class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -298,16 +278,6 @@ class IPAddressFilter(CustomFieldFilterSet): to_field_name='rd', label='VRF (RD)', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) device = django_filters.CharFilter( method='filter_device', field_name='name', @@ -329,6 +299,12 @@ class IPAddressFilter(CustomFieldFilterSet): to_field_name='name', label='Virtual machine (name)', ) + interface = django_filters.ModelMultipleChoiceFilter( + field_name='interface__name', + queryset=Interface.objects.all(), + to_field_name='name', + label='Interface (ID)', + ) interface_id = django_filters.ModelMultipleChoiceFilter( queryset=Interface.objects.all(), label='Interface (ID)', @@ -408,7 +384,7 @@ class VLANGroupFilter(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug'] -class VLANFilter(CustomFieldFilterSet): +class VLANFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -437,16 +413,6 @@ class VLANFilter(CustomFieldFilterSet): to_field_name='slug', label='Group', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) role_id = django_filters.ModelMultipleChoiceFilter( queryset=Role.objects.all(), label='Role (ID)', diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 2ff5f467f..7a79caaa0 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -6,6 +6,7 @@ from taggit.forms import TagField from dcim.models import Site, Rack, Device, Interface from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from tenancy.forms import TenancyForm +from tenancy.forms import TenancyFilterForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, @@ -97,22 +98,13 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm ] -class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm): +class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VRF + field_order = ['q', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) # @@ -497,8 +489,12 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF ] -class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): +class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Prefix + field_order = [ + 'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'site', 'role', 'tenant_group', 'tenant', + 'is_pool', 'expand', + ] q = forms.CharField( required=False, label='Search' @@ -533,16 +529,6 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) status = forms.MultipleChoiceField( choices=PREFIX_STATUS_CHOICES, required=False, @@ -949,8 +935,11 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form): ) -class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): +class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = IPAddress + field_order = [ + 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'tenant_group', 'tenant', + ] q = forms.CharField( required=False, label='Search' @@ -985,16 +974,6 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) status = forms.MultipleChoiceField( choices=IPADDRESS_STATUS_CHOICES, required=False, @@ -1226,8 +1205,9 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor ] -class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): +class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VLAN + field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' @@ -1251,16 +1231,6 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) status = forms.MultipleChoiceField( choices=VLAN_STATUS_CHOICES, required=False, diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 591b54c0c..3906f080f 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -319,6 +319,7 @@ class PrefixTable(BaseTable): class PrefixDetailTable(PrefixTable): utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(PrefixTable.Meta): fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description') @@ -351,6 +352,7 @@ class IPAddressDetailTable(IPAddressTable): nat_inside = tables.LinkColumn( 'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)' ) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(IPAddressTable.Meta): fields = ( @@ -426,6 +428,7 @@ class VLANTable(BaseTable): class VLANDetailTable(VLANTable): prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes') + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(VLANTable.Meta): fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description') diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 391ba7310..29368090e 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -1,3 +1,5 @@ +import json + from django.urls import reverse from netaddr import IPNetwork from rest_framework import status @@ -870,6 +872,8 @@ class VLANTest(APITestCase): self.vlan2 = VLAN.objects.create(vid=2, name='Test VLAN 2') self.vlan3 = VLAN.objects.create(vid=3, name='Test VLAN 3') + self.prefix1 = Prefix.objects.create(prefix=IPNetwork('192.168.1.0/24')) + def test_get_vlan(self): url = reverse('ipam-api:vlan-detail', kwargs={'pk': self.vlan1.pk}) @@ -960,6 +964,20 @@ class VLANTest(APITestCase): self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) self.assertEqual(VLAN.objects.count(), 2) + def test_delete_vlan_with_prefix(self): + self.prefix1.vlan = self.vlan1 + self.prefix1.save() + + url = reverse('ipam-api:vlan-detail', kwargs={'pk': self.vlan1.pk}) + response = self.client.delete(url, **self.header) + + # can't use assertHttpStatus here because we don't have response.data + self.assertEqual(response.status_code, 409) + + content = json.loads(response.content.decode('utf-8')) + self.assertIn('detail', content) + self.assertTrue(content['detail'].startswith('Unable to delete object.')) + class ServiceTest(APITestCase): diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index c2f7badd3..2a1dcdf05 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from extras.views import ObjectChangeLogView from . import views @@ -8,97 +8,97 @@ app_name = 'ipam' urlpatterns = [ # VRFs - url(r'^vrfs/$', views.VRFListView.as_view(), name='vrf_list'), - url(r'^vrfs/add/$', views.VRFCreateView.as_view(), name='vrf_add'), - 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.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'), - url(r'^vrfs/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}), + path(r'vrfs/', views.VRFListView.as_view(), name='vrf_list'), + path(r'vrfs/add/', views.VRFCreateView.as_view(), name='vrf_add'), + path(r'vrfs/import/', views.VRFBulkImportView.as_view(), name='vrf_import'), + path(r'vrfs/edit/', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'), + path(r'vrfs/delete/', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'), + path(r'vrfs//', views.VRFView.as_view(), name='vrf'), + path(r'vrfs//edit/', views.VRFEditView.as_view(), name='vrf_edit'), + path(r'vrfs//delete/', views.VRFDeleteView.as_view(), name='vrf_delete'), + path(r'vrfs//changelog/', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}), # RIRs - url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'), - url(r'^rirs/add/$', views.RIRCreateView.as_view(), name='rir_add'), - url(r'^rirs/import/$', views.RIRBulkImportView.as_view(), name='rir_import'), - url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'), - url(r'^rirs/(?P[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'), - url(r'^vrfs/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}), + path(r'rirs/', views.RIRListView.as_view(), name='rir_list'), + path(r'rirs/add/', views.RIRCreateView.as_view(), name='rir_add'), + path(r'rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'), + path(r'rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'), + path(r'rirs//edit/', views.RIREditView.as_view(), name='rir_edit'), + path(r'vrfs//changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}), # Aggregates - url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'), - url(r'^aggregates/add/$', views.AggregateCreateView.as_view(), name='aggregate_add'), - 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.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'), - url(r'^aggregates/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}), + path(r'aggregates/', views.AggregateListView.as_view(), name='aggregate_list'), + path(r'aggregates/add/', views.AggregateCreateView.as_view(), name='aggregate_add'), + path(r'aggregates/import/', views.AggregateBulkImportView.as_view(), name='aggregate_import'), + path(r'aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'), + path(r'aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'), + path(r'aggregates//', views.AggregateView.as_view(), name='aggregate'), + path(r'aggregates//edit/', views.AggregateEditView.as_view(), name='aggregate_edit'), + path(r'aggregates//delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'), + path(r'aggregates//changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}), # Roles - url(r'^roles/$', views.RoleListView.as_view(), name='role_list'), - url(r'^roles/add/$', views.RoleCreateView.as_view(), name='role_add'), - url(r'^roles/import/$', views.RoleBulkImportView.as_view(), name='role_import'), - url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'), - url(r'^roles/(?P[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'), - url(r'^roles/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}), + path(r'roles/', views.RoleListView.as_view(), name='role_list'), + path(r'roles/add/', views.RoleCreateView.as_view(), name='role_add'), + path(r'roles/import/', views.RoleBulkImportView.as_view(), name='role_import'), + path(r'roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'), + path(r'roles//edit/', views.RoleEditView.as_view(), name='role_edit'), + path(r'roles//changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}), # Prefixes - url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'), - url(r'^prefixes/add/$', views.PrefixCreateView.as_view(), name='prefix_add'), - 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.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+)/changelog/$', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}), - url(r'^prefixes/(?P\d+)/prefixes/$', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'), - url(r'^prefixes/(?P\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'), + path(r'prefixes/', views.PrefixListView.as_view(), name='prefix_list'), + path(r'prefixes/add/', views.PrefixCreateView.as_view(), name='prefix_add'), + path(r'prefixes/import/', views.PrefixBulkImportView.as_view(), name='prefix_import'), + path(r'prefixes/edit/', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'), + path(r'prefixes/delete/', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'), + path(r'prefixes//', views.PrefixView.as_view(), name='prefix'), + path(r'prefixes//edit/', views.PrefixEditView.as_view(), name='prefix_edit'), + path(r'prefixes//delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'), + path(r'prefixes//changelog/', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}), + path(r'prefixes//prefixes/', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'), + path(r'prefixes//ip-addresses/', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'), # IP addresses - url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'), - url(r'^ip-addresses/add/$', views.IPAddressCreateView.as_view(), name='ipaddress_add'), - url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'), - 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+)/changelog/$', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}), - url(r'^ip-addresses/assign/$', views.IPAddressAssignView.as_view(), name='ipaddress_assign'), - 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'), + path(r'ip-addresses/', views.IPAddressListView.as_view(), name='ipaddress_list'), + path(r'ip-addresses/add/', views.IPAddressCreateView.as_view(), name='ipaddress_add'), + path(r'ip-addresses/bulk-add/', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'), + path(r'ip-addresses/import/', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), + path(r'ip-addresses/edit/', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'), + path(r'ip-addresses/delete/', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), + path(r'ip-addresses//changelog/', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}), + path(r'ip-addresses/assign/', views.IPAddressAssignView.as_view(), name='ipaddress_assign'), + path(r'ip-addresses//', views.IPAddressView.as_view(), name='ipaddress'), + path(r'ip-addresses//edit/', views.IPAddressEditView.as_view(), name='ipaddress_edit'), + path(r'ip-addresses//delete/', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), # VLAN groups - url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'), - url(r'^vlan-groups/add/$', views.VLANGroupCreateView.as_view(), name='vlangroup_add'), - url(r'^vlan-groups/import/$', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'), - url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'), - url(r'^vlan-groups/(?P\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), - url(r'^vlan-groups/(?P\d+)/vlans/$', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'), - url(r'^vlan-groups/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}), + path(r'vlan-groups/', views.VLANGroupListView.as_view(), name='vlangroup_list'), + path(r'vlan-groups/add/', views.VLANGroupCreateView.as_view(), name='vlangroup_add'), + path(r'vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'), + path(r'vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'), + path(r'vlan-groups//edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), + path(r'vlan-groups//vlans/', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'), + path(r'vlan-groups//changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}), # VLANs - url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'), - url(r'^vlans/add/$', views.VLANCreateView.as_view(), name='vlan_add'), - 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.VLANView.as_view(), name='vlan'), - url(r'^vlans/(?P\d+)/members/$', views.VLANMembersView.as_view(), name='vlan_members'), - 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'), - url(r'^vlans/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}), + path(r'vlans/', views.VLANListView.as_view(), name='vlan_list'), + path(r'vlans/add/', views.VLANCreateView.as_view(), name='vlan_add'), + path(r'vlans/import/', views.VLANBulkImportView.as_view(), name='vlan_import'), + path(r'vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'), + path(r'vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'), + path(r'vlans//', views.VLANView.as_view(), name='vlan'), + path(r'vlans//members/', views.VLANMembersView.as_view(), name='vlan_members'), + path(r'vlans//edit/', views.VLANEditView.as_view(), name='vlan_edit'), + path(r'vlans//delete/', views.VLANDeleteView.as_view(), name='vlan_delete'), + path(r'vlans//changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}), # Services - url(r'^services/$', views.ServiceListView.as_view(), name='service_list'), - url(r'^services/edit/$', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'), - url(r'^services/delete/$', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'), - url(r'^services/(?P\d+)/$', views.ServiceView.as_view(), name='service'), - url(r'^services/(?P\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'), - url(r'^services/(?P\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'), - url(r'^services/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}), + path(r'services/', views.ServiceListView.as_view(), name='service_list'), + path(r'services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'), + path(r'services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'), + path(r'services//', views.ServiceView.as_view(), name='service'), + path(r'services//edit/', views.ServiceEditView.as_view(), name='service_edit'), + path(r'services//delete/', views.ServiceDeleteView.as_view(), name='service_delete'), + path(r'services//changelog/', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}), ] diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index aaef05e00..ef2c9ec28 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -1,5 +1,6 @@ from django.conf import settings -from django.conf.urls import include, url +from django.conf.urls import include +from django.urls import path, re_path from django.views.static import serve from drf_yasg import openapi from drf_yasg.views import get_schema_view @@ -24,63 +25,63 @@ schema_view = get_schema_view( _patterns = [ # Base views - url(r'^$', HomeView.as_view(), name='home'), - url(r'^search/$', SearchView.as_view(), name='search'), + path(r'', HomeView.as_view(), name='home'), + path(r'search/', SearchView.as_view(), name='search'), # Login/logout - url(r'^login/$', LoginView.as_view(), name='login'), - url(r'^logout/$', LogoutView.as_view(), name='logout'), + path(r'login/', LoginView.as_view(), name='login'), + path(r'logout/', LogoutView.as_view(), name='logout'), # Apps - url(r'^circuits/', include('circuits.urls')), - url(r'^dcim/', include('dcim.urls')), - url(r'^extras/', include('extras.urls')), - url(r'^ipam/', include('ipam.urls')), - url(r'^secrets/', include('secrets.urls')), - url(r'^tenancy/', include('tenancy.urls')), - url(r'^user/', include('users.urls')), - url(r'^virtualization/', include('virtualization.urls')), + path(r'circuits/', include('circuits.urls')), + path(r'dcim/', include('dcim.urls')), + path(r'extras/', include('extras.urls')), + path(r'ipam/', include('ipam.urls')), + path(r'secrets/', include('secrets.urls')), + path(r'tenancy/', include('tenancy.urls')), + path(r'user/', include('users.urls')), + path(r'virtualization/', include('virtualization.urls')), # API - url(r'^api/$', APIRootView.as_view(), name='api-root'), - url(r'^api/circuits/', include('circuits.api.urls')), - url(r'^api/dcim/', include('dcim.api.urls')), - url(r'^api/extras/', include('extras.api.urls')), - url(r'^api/ipam/', include('ipam.api.urls')), - url(r'^api/secrets/', include('secrets.api.urls')), - url(r'^api/tenancy/', include('tenancy.api.urls')), - url(r'^api/virtualization/', include('virtualization.api.urls')), - url(r'^api/docs/$', schema_view.with_ui('swagger'), name='api_docs'), - url(r'^api/redoc/$', schema_view.with_ui('redoc'), name='api_redocs'), - url(r'^api/swagger(?P.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'), + path(r'api/', APIRootView.as_view(), name='api-root'), + path(r'api/circuits/', include('circuits.api.urls')), + path(r'api/dcim/', include('dcim.api.urls')), + path(r'api/extras/', include('extras.api.urls')), + path(r'api/ipam/', include('ipam.api.urls')), + path(r'api/secrets/', include('secrets.api.urls')), + path(r'api/tenancy/', include('tenancy.api.urls')), + path(r'api/virtualization/', include('virtualization.api.urls')), + path(r'api/docs/', schema_view.with_ui('swagger'), name='api_docs'), + path(r'api/redoc/', schema_view.with_ui('redoc'), name='api_redocs'), + re_path(r'^api/swagger(?P.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'), # Serving static media in Django to pipe it through LoginRequiredMiddleware - url(r'^media/(?P.*)$', serve, {'document_root': settings.MEDIA_ROOT}), + path(r'media/', serve, {'document_root': settings.MEDIA_ROOT}), # Admin - url(r'^admin/', admin_site.urls), + path(r'admin/', admin_site.urls), ] if settings.WEBHOOKS_ENABLED: _patterns += [ - url(r'^admin/webhook-backend-status/', include('django_rq.urls')), + path(r'admin/webhook-backend-status/', include('django_rq.urls')), ] if settings.DEBUG: import debug_toolbar _patterns += [ - url(r'^__debug__/', include(debug_toolbar.urls)), + path(r'__debug__/', include(debug_toolbar.urls)), ] if settings.METRICS_ENABLED: _patterns += [ - url('', include('django_prometheus.urls')), + path('', include('django_prometheus.urls')), ] # Prepend BASE_PATH urlpatterns = [ - url(r'^{}'.format(settings.BASE_PATH), include(_patterns)) + path(r'{}'.format(settings.BASE_PATH), include(_patterns)) ] handler500 = 'utilities.views.server_error' diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index 26ca50220..e87811bc1 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -559,6 +559,7 @@ table.report th a { .color-block { display: block; width: 80px; + border: 1px solid grey; } .text-nowrap { white-space: nowrap; diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 96d59ace5..e8cc6aa1f 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -267,6 +267,10 @@ $(document).ready(function() { processResults: function (data) { var results = $.map(data.results, function (obj) { + // If tag contains space add double quotes + if (/\s/.test(obj.name)) + obj.name = '"' + obj.name + '"' + return { id: obj.name, text: obj.name diff --git a/netbox/secrets/urls.py b/netbox/secrets/urls.py index e1ce2b8f2..9d07dd63c 100644 --- a/netbox/secrets/urls.py +++ b/netbox/secrets/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from extras.views import ObjectChangeLogView from . import views @@ -8,21 +8,21 @@ app_name = 'secrets' urlpatterns = [ # Secret roles - url(r'^secret-roles/$', views.SecretRoleListView.as_view(), name='secretrole_list'), - url(r'^secret-roles/add/$', views.SecretRoleCreateView.as_view(), name='secretrole_add'), - url(r'^secret-roles/import/$', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'), - url(r'^secret-roles/delete/$', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'), - url(r'^secret-roles/(?P[\w-]+)/edit/$', views.SecretRoleEditView.as_view(), name='secretrole_edit'), - url(r'^secret-roles/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}), + path(r'secret-roles/', views.SecretRoleListView.as_view(), name='secretrole_list'), + path(r'secret-roles/add/', views.SecretRoleCreateView.as_view(), name='secretrole_add'), + path(r'secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'), + path(r'secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'), + path(r'secret-roles//edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'), + path(r'secret-roles//changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}), # Secrets - url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'), - url(r'^secrets/import/$', views.SecretBulkImportView.as_view(), 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.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'), - url(r'^secrets/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}), + path(r'secrets/', views.SecretListView.as_view(), name='secret_list'), + path(r'secrets/import/', views.SecretBulkImportView.as_view(), name='secret_import'), + path(r'secrets/edit/', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'), + path(r'secrets/delete/', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'), + path(r'secrets//', views.SecretView.as_view(), name='secret'), + path(r'secrets//edit/', views.secret_edit, name='secret_edit'), + path(r'secrets//delete/', views.SecretDeleteView.as_view(), name='secret_delete'), + path(r'secrets//changelog/', ObjectChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}), ] diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index 7ef88543e..08ecf783d 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -31,7 +31,7 @@

{{ cable.get_status_display }}

{{ cable.get_type_display|default:"" }}

- {% if cable.length %}- {{ cable.length }}{{ cable.get_length_unit_display }}{% endif %} + {% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %}   {% else %}

No Cable

diff --git a/netbox/templates/dcim/site_list.html b/netbox/templates/dcim/site_list.html index 0dd52a96d..64948a6f9 100644 --- a/netbox/templates/dcim/site_list.html +++ b/netbox/templates/dcim/site_list.html @@ -12,7 +12,7 @@

{% block title %}Sites{% endblock %}

- {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' %} + {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' bulk_delete_url='dcim:site_bulk_delete' %}
{% include 'inc/search_panel.html' %} diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index 76dc61ef3..b8e0d6dcb 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -435,7 +435,12 @@ {% else %} -
  • Log in
  • + {% url 'login' as login_url %} + {% if request.path == login_url %} +
  • Log in
  • + {% else %} +
  • Log in
  • + {% endif %} {% endif %}