From f78c228c757832abd7fabe41fa90ecd1d9eda3ea Mon Sep 17 00:00:00 2001 From: dansheps Date: Sat, 23 Feb 2019 10:37:30 -0600 Subject: [PATCH 01/43] Fixes #2813: Add Filter for TenantGroup to the following Forms and Filter classes: * circuit.Circuit * dcim.Site * dcim.Rack * dcim.RackElevation * dcim.RackReservation * dcim.Device * ipam.IPAddress * ipam.Prefix * ipam.VRF * ipam.VLAN * virtualization.VirtualMachine --- netbox/circuits/filters.py | 14 ++++++++- netbox/circuits/forms.py | 15 ++++++++- netbox/dcim/filters.py | 50 ++++++++++++++++++++++++++++- netbox/dcim/forms.py | 54 +++++++++++++++++++++++++++++++- netbox/ipam/filters.py | 50 ++++++++++++++++++++++++++++- netbox/ipam/forms.py | 54 +++++++++++++++++++++++++++++++- netbox/virtualization/filters.py | 14 ++++++++- netbox/virtualization/forms.py | 15 ++++++++- 8 files changed, 258 insertions(+), 8 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 12955eeca..f970c828e 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.models import Tenant, TenantGroup from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from .constants import CIRCUIT_STATUS_CHOICES from .models import Provider, Circuit, CircuitTermination, CircuitType @@ -87,6 +87,18 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): choices=CIRCUIT_STATUS_CHOICES, null_value=None ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 4deee57c9..2a508b3c2 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -4,7 +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.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple @@ -292,6 +292,19 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, widget=StaticSelect2Multiple() ) + 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', diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 96ecefafd..4710800c5 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -6,7 +6,7 @@ from netaddr import EUI from netaddr.core import AddrFormatError from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.constants import COLOR_CHOICES from utilities.filters import NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter from virtualization.models import Cluster @@ -59,6 +59,18 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): field_name='slug', label='Region (slug)', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', @@ -160,6 +172,18 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Group', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', @@ -241,6 +265,18 @@ class RackReservationFilter(django_filters.FilterSet): to_field_name='slug', label='Group', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', @@ -491,6 +527,18 @@ class DeviceFilter(CustomFieldFilterSet): to_field_name='slug', label='Role (slug)', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index ad209c516..9c7060bd7 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -13,7 +13,7 @@ from timezone_field import TimeZoneFormField 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.models import Tenant, TenantGroup from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, @@ -276,6 +276,19 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): 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', @@ -619,6 +632,19 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) + 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', @@ -711,6 +737,19 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form): null_option=True, ) ) + 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', @@ -1703,6 +1742,19 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) + 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', diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index f7125ceb0..9e6d4006e 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -6,7 +6,7 @@ from netaddr.core import AddrFormatError from dcim.models import Site, Device, Interface from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup 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 @@ -22,6 +22,18 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): method='search', label='Search', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', @@ -146,6 +158,18 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='rd', label='VRF (RD)', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', @@ -285,6 +309,18 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='rd', label='VRF (RD)', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', @@ -423,6 +459,18 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Group', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index d0e25f580..8bc207527 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -6,7 +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.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm, SlugField, @@ -103,6 +103,19 @@ class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) + 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', @@ -535,6 +548,19 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) + 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', @@ -984,6 +1010,19 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) + 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', @@ -1250,6 +1289,19 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) + 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', diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 0b7e57ba7..32af27adc 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -6,7 +6,7 @@ from netaddr.core import AddrFormatError from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -150,6 +150,18 @@ class VirtualMachineFilter(CustomFieldFilterSet): to_field_name='slug', label='Role (slug)', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 70bbf0910..931dccc5d 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -8,7 +8,7 @@ from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, S from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm from ipam.models import IPAddress from tenancy.forms import TenancyForm -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm, @@ -591,6 +591,19 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, widget=StaticSelect2Multiple() ) + 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', From 8683efe54ae438b589fcd9fdb580956d4024abef Mon Sep 17 00:00:00 2001 From: dansheps Date: Sat, 23 Feb 2019 11:09:02 -0600 Subject: [PATCH 02/43] Fixes #2813: Add Filter and List View for TenantGroup Added Filter for TenantGroup to the following Forms and Filter classes * circuit.Circuit * dcim.Site * dcim.Rack * dcim.RackElevation * dcim.RackReservation * dcim.Device * ipam.IPAddress * ipam.Prefix * ipam.VRF * ipam.VLAN * virtualization.VirtualMachine Added List View to the following classes: * circuit.Circuit * dcim.Site * dcim.Rack * dcim.RackReservation * dcim.Device * ipam.IPAddress * ipam.Prefix * ipam.VRF * ipam.VLAN * virtualization.VirtualMachine --- netbox/circuits/tables.py | 4 ++-- netbox/dcim/tables.py | 10 +++++----- netbox/ipam/tables.py | 15 +++++++++++---- netbox/tenancy/tables.py | 10 ++++++++++ netbox/virtualization/tables.py | 4 ++-- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index c6a215db8..f90a761a7 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django.utils.safestring import mark_safe from django_tables2.utils import Accessor -from tenancy.tables import COL_TENANT +from tenancy.tables import COL_TENANTGROUP_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import Circuit, CircuitType, Provider @@ -76,7 +76,7 @@ class CircuitTable(BaseTable): cid = tables.LinkColumn(verbose_name='ID') provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')]) status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side') termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side') diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 5649c10ef..11dcca81a 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from tenancy.tables import COL_TENANT +from tenancy.tables import COL_TENANT, COL_TENANTGROUP_TENANT from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -214,7 +214,7 @@ class SiteTable(BaseTable): name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3')) status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') region = tables.TemplateColumn(template_code=SITE_REGION_LINK) - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) class Meta(BaseTable.Meta): model = Site @@ -275,7 +275,7 @@ class RackTable(BaseTable): name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3')) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) status = tables.TemplateColumn(STATUS_LABEL) role = tables.TemplateColumn(RACK_ROLE) u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') @@ -305,7 +305,7 @@ class RackDetailTable(RackTable): class RackReservationTable(BaseTable): pk = ToggleColumn() - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) unit_list = tables.Column(orderable=False, verbose_name='Units') actions = tables.TemplateColumn( @@ -512,7 +512,7 @@ class DeviceTable(BaseTable): template_code=DEVICE_LINK ) status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role') diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 026cbc980..2ae9d562a 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Interface -from tenancy.tables import COL_TENANT +from tenancy.tables import COL_TENANT,COL_TENANTGROUP_TENANT from utilities.tables import BaseTable, BooleanColumn, ToggleColumn from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF @@ -169,8 +169,12 @@ VLAN_MEMBER_ACTIONS = """ """ TENANT_LINK = """ -{% if record.tenant %} +{% if record.tenant and record.tenant.group %} + {{record.tenant.group}}:{{ record.tenant }} +{% elif record.tenant %} {{ record.tenant }} +{% elif record.vrf.tenant.group %} + {{record.vrf.tenant.group}}:{{ record.vrf.tenant }}* {% elif record.vrf.tenant %} {{ record.vrf.tenant }}* {% else %} @@ -187,7 +191,7 @@ class VRFTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() rd = tables.Column(verbose_name='RD') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) class Meta(BaseTable.Meta): model = VRF @@ -319,6 +323,7 @@ class PrefixTable(BaseTable): class PrefixDetailTable(PrefixTable): utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) class Meta(PrefixTable.Meta): fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description') @@ -349,6 +354,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_TENANTGROUP_TENANT) class Meta(IPAddressTable.Meta): fields = ( @@ -409,7 +415,7 @@ class VLANTable(BaseTable): vid = tables.TemplateColumn(VLAN_LINK, verbose_name='ID') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.LinkColumn('ipam:vlangroup_vlans', args=[Accessor('group.pk')], verbose_name='Group') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) status = tables.TemplateColumn(STATUS_LABEL) role = tables.TemplateColumn(VLAN_ROLE_LINK) @@ -423,6 +429,7 @@ class VLANTable(BaseTable): class VLANDetailTable(VLANTable): prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes') + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) class Meta(VLANTable.Meta): fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description') diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 91122df7a..779d3bd2e 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -20,6 +20,16 @@ COL_TENANT = """ {% endif %} """ +COL_TENANTGROUP_TENANT = """ +{% if record.tenant and record.tenant.group %} + {{record.tenant.group}}:{{ record.tenant }} +{% elif record.tenant %} + {{ record.tenant }} +{% else %} + — +{% endif %} +""" + # # Tenant groups diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index b825ba59f..354f9025d 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Interface -from tenancy.tables import COL_TENANT +from tenancy.tables import COL_TENANTGROUP_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -103,7 +103,7 @@ class VirtualMachineTable(BaseTable): status = tables.TemplateColumn(template_code=VIRTUALMACHINE_STATUS) cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')]) role = tables.TemplateColumn(VIRTUALMACHINE_ROLE) - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) class Meta(BaseTable.Meta): model = VirtualMachine From 679aa0f764d6b0c830d093cd59c9a753757c49e0 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 26 Feb 2019 07:53:59 -0600 Subject: [PATCH 03/43] Update tables.py Fix whitespace --- netbox/ipam/tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 2ae9d562a..ff8cf8928 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Interface -from tenancy.tables import COL_TENANT,COL_TENANTGROUP_TENANT +from tenancy.tables import COL_TENANT, COL_TENANTGROUP_TENANT from utilities.tables import BaseTable, BooleanColumn, ToggleColumn from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF From b4d7f9ea43564567b19a8dbe203d1b21d3f64aff Mon Sep 17 00:00:00 2001 From: dansheps Date: Tue, 5 Mar 2019 08:10:10 -0600 Subject: [PATCH 04/43] Fixes #2781: Fixes filter by regions on site and device list * Add Device filter --- netbox/circuits/filters.py | 26 +------ netbox/circuits/forms.py | 31 ++------- netbox/circuits/tables.py | 4 +- netbox/dcim/filters.py | 111 ++---------------------------- netbox/dcim/forms.py | 112 ++++--------------------------- netbox/dcim/tables.py | 10 +-- netbox/ipam/filters.py | 98 ++------------------------- netbox/ipam/forms.py | 112 ++++--------------------------- netbox/ipam/tables.py | 12 ++-- netbox/tenancy/filters.py | 25 +++++++ netbox/tenancy/forms.py | 28 ++++++++ netbox/virtualization/filters.py | 26 +------ netbox/virtualization/forms.py | 31 ++------- netbox/virtualization/tables.py | 4 +- 14 files changed, 121 insertions(+), 509 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index f970c828e..f54567f68 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, TenantGroup +from tenancy.filters 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 = ['name', 'slug'] -class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): +class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -87,28 +87,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): choices=CIRCUIT_STATUS_CHOICES, null_value=None ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (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)', - ) 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 2a508b3c2..95237ab1c 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -3,8 +3,8 @@ 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.models import Tenant, TenantGroup +from tenancy.forms import TenancyForm, TenancyFilterForm +from tenancy.models import Tenant from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple @@ -265,8 +265,10 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit ] -class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): +class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Circuit + # Order the form fields, fields not listed are appended + field_order = ['q', 'type', 'provider', 'status'] q = forms.CharField( required=False, label='Search' @@ -292,29 +294,6 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, widget=StaticSelect2Multiple() ) - 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, - ) - ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index f90a761a7..c6a215db8 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django.utils.safestring import mark_safe from django_tables2.utils import Accessor -from tenancy.tables import COL_TENANTGROUP_TENANT +from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import Circuit, CircuitType, Provider @@ -76,7 +76,7 @@ class CircuitTable(BaseTable): cid = tables.LinkColumn(verbose_name='ID') provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')]) status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side') termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side') diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 4710800c5..7d609426d 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -6,7 +6,7 @@ from netaddr import EUI from netaddr.core import AddrFormatError from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant, TenantGroup +from tenancy.filters import TenancyFilterSet from utilities.constants import COLOR_CHOICES from utilities.filters import NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter from virtualization.models import Cluster @@ -36,7 +36,7 @@ class RegionFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): +class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -59,28 +59,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): field_name='slug', label='Region (slug)', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (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: @@ -142,7 +120,7 @@ class RackRoleFilter(NameSlugSearchFilterSet): fields = ['name', 'slug', 'color'] -class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): +class RackFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -172,28 +150,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Group', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (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)', - ) status = django_filters.MultipleChoiceFilter( choices=RACK_STATUS_CHOICES, null_value=None @@ -230,7 +186,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): ) -class RackReservationFilter(django_filters.FilterSet): +class RackReservationFilter(TenancyFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -265,28 +221,6 @@ class RackReservationFilter(django_filters.FilterSet): to_field_name='slug', label='Group', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (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)', - ) user_id = django_filters.ModelMultipleChoiceFilter( queryset=User.objects.all(), label='User (ID)', @@ -492,7 +426,7 @@ class PlatformFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class DeviceFilter(CustomFieldFilterSet): +class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -527,28 +461,6 @@ class DeviceFilter(CustomFieldFilterSet): to_field_name='slug', label='Role (slug)', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (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)', @@ -978,7 +890,7 @@ class InventoryItemFilter(DeviceComponentFilterSet): return queryset.filter(qs_filter) -class VirtualChassisFilter(django_filters.FilterSet): +class VirtualChassisFilter(TenancyFilterSet, django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -994,17 +906,6 @@ class VirtualChassisFilter(django_filters.FilterSet): to_field_name='slug', label='Site name (slug)', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - field_name='master__tenant', - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='master__tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) tag = TagFilter() class Meta: diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 9c7060bd7..f851e0938 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -12,8 +12,8 @@ from timezone_field import TimeZoneFormField 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, TenantGroup +from tenancy.forms import TenancyForm, TenancyFilterForm +from tenancy.models import Tenant from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, @@ -256,8 +256,10 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor ] -class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): +class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Site + # Order the form fields, fields not listed are appended + field_order = ['q', 'status', 'region'] q = forms.CharField( required=False, label='Search' @@ -276,29 +278,6 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): 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, - ) - ) # @@ -609,8 +588,10 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor ] -class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): +class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Rack + # Order the form fields, fields not listed are appended + field_order = ['q', 'site', 'group_id'] q = forms.CharField( required=False, label='Search' @@ -632,29 +613,6 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - 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, - ) - ) status = forms.MultipleChoiceField( choices=RACK_STATUS_CHOICES, required=False, @@ -715,7 +673,9 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm): return unit_choices -class RackReservationFilterForm(BootstrapMixin, forms.Form): +class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, forms.Form): + # Order the form fields, fields not listed are appended + field_order = ['q', 'site', 'group_id'] q = forms.CharField( required=False, label='Search' @@ -737,29 +697,6 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form): null_option=True, ) ) - 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, - ) - ) class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): @@ -1682,8 +1619,10 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF ] -class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): +class DeviceFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Device + # Order the form fields, fields not listed are appended + field_order = ['q', 'region', 'site', 'rack_group_id', 'rack_id', 'role'] q = forms.CharField( required=False, label='Search' @@ -1742,29 +1681,6 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - 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, - ) - ) manufacturer_id = FilterChoiceField( queryset=Manufacturer.objects.all(), label='Manufacturer', diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 11dcca81a..b61bfed64 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from tenancy.tables import COL_TENANT, COL_TENANTGROUP_TENANT +from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -214,7 +214,7 @@ class SiteTable(BaseTable): name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3')) status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') region = tables.TemplateColumn(template_code=SITE_REGION_LINK) - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(BaseTable.Meta): model = Site @@ -275,7 +275,7 @@ class RackTable(BaseTable): name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3')) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) status = tables.TemplateColumn(STATUS_LABEL) role = tables.TemplateColumn(RACK_ROLE) u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') @@ -305,7 +305,7 @@ class RackDetailTable(RackTable): class RackReservationTable(BaseTable): pk = ToggleColumn() - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + 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( @@ -512,7 +512,7 @@ class DeviceTable(BaseTable): template_code=DEVICE_LINK ) status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role') diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 9e6d4006e..da60c9f3d 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, TenantGroup +from tenancy.filters 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, django_filters.FilterSet): +class VRFFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -22,28 +22,6 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): method='search', label='Search', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (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() def search(self, queryset, name, value): @@ -119,7 +97,7 @@ class RoleFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): +class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -158,28 +136,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='rd', label='VRF (RD)', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (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)', - ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -278,7 +234,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): return queryset.filter(prefix__net_mask_length=value) -class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): +class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -309,28 +265,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='rd', label='VRF (RD)', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (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)', - ) device = django_filters.CharFilter( method='filter_device', field_name='name', @@ -430,7 +364,7 @@ class VLANGroupFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): +class VLANFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -459,28 +393,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Group', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (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)', - ) 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 8bc207527..6b97c1f83 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -5,8 +5,8 @@ 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.models import Tenant, TenantGroup +from tenancy.forms import TenancyForm, TenancyFilterForm +from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm, SlugField, @@ -97,35 +97,14 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm ] -class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm): +class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VRF + # Order the form fields, fields not listed are appended + field_order = ['q'] q = forms.CharField( required=False, label='Search' ) - 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, - ) - ) # @@ -510,8 +489,10 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF ] -class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): +class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Prefix + # Order the form fields, fields not listed are appended + field_order = ['q', 'within_include', 'family', 'mask_length', 'vrf'] q = forms.CharField( required=False, label='Search' @@ -548,29 +529,6 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - 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, - ) - ) status = forms.MultipleChoiceField( choices=PREFIX_STATUS_CHOICES, required=False, @@ -972,8 +930,10 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form): ) -class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): +class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = IPAddress + # Order the form fields, fields not listed are appended + field_order = ['q', 'parent', 'family', 'mask_length', 'vrf'] q = forms.CharField( required=False, label='Search' @@ -1010,29 +970,6 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - 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, - ) - ) status = forms.MultipleChoiceField( choices=IPADDRESS_STATUS_CHOICES, required=False, @@ -1264,8 +1201,10 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor ] -class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): +class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VLAN + # Order the form fields, fields not listed are appended + field_order = ['q', 'site', 'group_id'] q = forms.CharField( required=False, label='Search' @@ -1289,29 +1228,6 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - 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, - ) - ) status = forms.MultipleChoiceField( choices=VLAN_STATUS_CHOICES, required=False, diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 2ae9d562a..7a5ad97c3 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Interface -from tenancy.tables import COL_TENANT,COL_TENANTGROUP_TENANT +from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, BooleanColumn, ToggleColumn from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF @@ -191,7 +191,7 @@ class VRFTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() rd = tables.Column(verbose_name='RD') - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(BaseTable.Meta): model = VRF @@ -323,7 +323,7 @@ class PrefixTable(BaseTable): class PrefixDetailTable(PrefixTable): utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False) - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(PrefixTable.Meta): fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description') @@ -354,7 +354,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_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(IPAddressTable.Meta): fields = ( @@ -415,7 +415,7 @@ class VLANTable(BaseTable): vid = tables.TemplateColumn(VLAN_LINK, verbose_name='ID') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.LinkColumn('ipam:vlangroup_vlans', args=[Accessor('group.pk')], verbose_name='Group') - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) status = tables.TemplateColumn(STATUS_LABEL) role = tables.TemplateColumn(VLAN_ROLE_LINK) @@ -429,7 +429,7 @@ class VLANTable(BaseTable): class VLANDetailTable(VLANTable): prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes') - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + 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/tenancy/filters.py b/netbox/tenancy/filters.py index 745391898..a7bf19f59 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -46,3 +46,28 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet): Q(description__icontains=value) | Q(comments__icontains=value) ) + + +class TenancyFilterSet(django_filters.FilterSet): + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (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)', + ) diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 3c97eb801..6e2c59cd4 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -115,6 +115,34 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): ) ) +# +# Tenancy filtering form extension +# +class TenancyFilterForm(forms.Form): + 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, + ) + ) + # # Tenancy form extension diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 32af27adc..5224d11ba 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -6,7 +6,7 @@ from netaddr.core import AddrFormatError from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant, TenantGroup +from tenancy.filters import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -80,7 +80,7 @@ class ClusterFilter(CustomFieldFilterSet): ) -class VirtualMachineFilter(CustomFieldFilterSet): +class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -150,28 +150,6 @@ class VirtualMachineFilter(CustomFieldFilterSet): to_field_name='slug', label='Role (slug)', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (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)', diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 931dccc5d..eb194e6b2 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -7,7 +7,7 @@ from dcim.forms import INTERFACE_MODE_HELP_TEXT from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm from ipam.models import IPAddress -from tenancy.forms import TenancyForm +from tenancy.forms import TenancyForm, TenancyFilterForm from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, @@ -336,7 +336,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): class Meta: model = VirtualMachine fields = [ - 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', + 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data', ] help_texts = { @@ -520,8 +520,10 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB ] -class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): +class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VirtualMachine + # Order the form fields, fields not listed are appended + field_order = ['q', 'cluster_group', 'cluster_type', 'cluster_id', 'region', 'site'] q = forms.CharField( required=False, label='Search' @@ -591,29 +593,6 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, widget=StaticSelect2Multiple() ) - 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, - ) - ) platform = FilterChoiceField( queryset=Platform.objects.all(), to_field_name='slug', diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index 354f9025d..b825ba59f 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Interface -from tenancy.tables import COL_TENANTGROUP_TENANT +from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -103,7 +103,7 @@ class VirtualMachineTable(BaseTable): status = tables.TemplateColumn(template_code=VIRTUALMACHINE_STATUS) cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')]) role = tables.TemplateColumn(VIRTUALMACHINE_ROLE) - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(BaseTable.Meta): model = VirtualMachine From 37811d3f7e3c04d7d2828f7695975e133fd01cbd Mon Sep 17 00:00:00 2001 From: dansheps Date: Tue, 5 Mar 2019 08:19:21 -0600 Subject: [PATCH 05/43] * Resolve conflict with virtualization filters. --- netbox/virtualization/filters.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 0e5ff6cd2..06b8f8553 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -6,7 +6,7 @@ from netaddr.core import AddrFormatError from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant +from tenancy.filters import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -80,7 +80,7 @@ class ClusterFilter(CustomFieldFilterSet): ) -class VirtualMachineFilter(CustomFieldFilterSet): +class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -151,16 +151,6 @@ class VirtualMachineFilter(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)', From 6e8e6809f344e335bd07834ae4cee35d4690c7a3 Mon Sep 17 00:00:00 2001 From: dansheps Date: Wed, 10 Apr 2019 08:37:12 -0500 Subject: [PATCH 06/43] Move Filter and Form to new file, update all files --- netbox/circuits/filters.py | 2 +- netbox/circuits/forms.py | 3 ++- netbox/dcim/filters.py | 2 +- netbox/dcim/forms.py | 3 ++- netbox/extras/filters.py | 26 ++------------------------ netbox/extras/forms.py | 24 ++++++------------------ netbox/ipam/filters.py | 2 +- netbox/ipam/forms.py | 3 ++- netbox/tenancy/filters.py | 25 ------------------------- netbox/tenancy/filterset.py | 27 +++++++++++++++++++++++++++ netbox/tenancy/forms.py | 32 +------------------------------- netbox/tenancy/formset.py | 31 +++++++++++++++++++++++++++++++ netbox/virtualization/filters.py | 2 +- netbox/virtualization/forms.py | 3 ++- 14 files changed, 79 insertions(+), 106 deletions(-) create mode 100644 netbox/tenancy/filterset.py create mode 100644 netbox/tenancy/formset.py diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index f54567f68..e79b78e7b 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.filters import TenancyFilterSet +from tenancy.filterset import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from .constants import CIRCUIT_STATUS_CHOICES from .models import Provider, Circuit, CircuitTermination, CircuitType diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 95237ab1c..f90cb9503 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -3,7 +3,8 @@ from taggit.forms import TagField from dcim.models import Site from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm -from tenancy.forms import TenancyForm, TenancyFilterForm +from tenancy.forms import TenancyForm +from tenancy.formset import TenancyFilterForm from tenancy.models import Tenant from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index dc2e411d6..2695cc7a5 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -5,7 +5,7 @@ from netaddr import EUI from netaddr.core import AddrFormatError from extras.filters import CustomFieldFilterSet -from tenancy.filters import TenancyFilterSet +from tenancy.filterset import TenancyFilterSet from utilities.constants import COLOR_CHOICES from utilities.filters import ( NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index bf40f172a..699e2635c 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -12,7 +12,8 @@ from timezone_field import TimeZoneFormField from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from ipam.models import IPAddress, VLAN, VLANGroup -from tenancy.forms import TenancyForm, TenancyFilterForm +from tenancy.forms import TenancyForm +from tenancy.formset import TenancyFilterForm from tenancy.models import Tenant from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index d5457a5a6..712dfd6d8 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -4,7 +4,7 @@ from django.db.models import Q from taggit.models import Tag from dcim.models import DeviceRole, Platform, Region, Site -from tenancy.models import Tenant, TenantGroup +from tenancy.filterset import TenancyFilterSet from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap @@ -122,7 +122,7 @@ class TopologyMapFilter(django_filters.FilterSet): fields = ['name', 'slug'] -class ConfigContextFilter(django_filters.FilterSet): +class ConfigContextFilter(TenancyFilterSet, django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -171,28 +171,6 @@ class ConfigContextFilter(django_filters.FilterSet): to_field_name='slug', label='Platform (slug)', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant_groups', - queryset=TenantGroup.objects.all(), - label='Tenant group', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant_groups__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant group (slug)', - ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenants', - queryset=Tenant.objects.all(), - label='Tenant', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenants__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) class Meta: model = ConfigContext diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 54eee0c5c..a956597d3 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -8,7 +8,7 @@ from taggit.forms import TagField from taggit.models import Tag from dcim.models import DeviceRole, Platform, Region, Site -from tenancy.models import Tenant, TenantGroup +from tenancy.formset import TenancyFilterForm from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, @@ -274,7 +274,7 @@ class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm): ] -class ConfigContextFilterForm(BootstrapMixin, forms.Form): +class ConfigContextFilterForm(TenancyFilterForm, BootstrapMixin, forms.Form): q = forms.CharField( required=False, label='Search' @@ -311,22 +311,10 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form): value_field="slug", ) ) - tenant_group = FilterChoiceField( - queryset=TenantGroup.objects.all(), - to_field_name='slug', - widget=APISelectMultiple( - api_url="/api/tenancy/tenant-groups/", - value_field="slug", - ) - ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - ) - ) + + class Meta: + # Order the form fields, fields not listed are appended + field_order = ['q', 'type', 'provider', 'status'] # diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index da60c9f3d..3050a7901 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -6,7 +6,7 @@ from netaddr.core import AddrFormatError from dcim.models import Site, Device, Interface from extras.filters import CustomFieldFilterSet -from tenancy.filters import TenancyFilterSet +from tenancy.filterset 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 diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index f731db353..1bc83a9d0 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -5,7 +5,8 @@ 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, TenancyFilterForm +from tenancy.forms import TenancyForm +from tenancy.formset import TenancyFilterForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index b2c8e7934..2610b3ec0 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -47,28 +47,3 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet): Q(description__icontains=value) | Q(comments__icontains=value) ) - - -class TenancyFilterSet(django_filters.FilterSet): - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (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)', - ) diff --git a/netbox/tenancy/filterset.py b/netbox/tenancy/filterset.py new file mode 100644 index 000000000..8980777b0 --- /dev/null +++ b/netbox/tenancy/filterset.py @@ -0,0 +1,27 @@ +import django_filters +from .models import Tenant, TenantGroup + + +class TenancyFilterSet(django_filters.FilterSet): + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (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)', + ) diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 6e2c59cd4..6095b1f5b 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -1,5 +1,4 @@ from django import forms -from django.db.models import Count from taggit.forms import TagField from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm @@ -115,39 +114,10 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): ) ) -# -# Tenancy filtering form extension -# -class TenancyFilterForm(forms.Form): - 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, - ) - ) - # # Tenancy form extension # - class TenancyForm(ChainedFieldsMixin, forms.Form): tenant_group = forms.ModelChoiceField( queryset=TenantGroup.objects.all(), @@ -182,4 +152,4 @@ class TenancyForm(ChainedFieldsMixin, forms.Form): initial['tenant_group'] = instance.tenant.group kwargs['initial'] = initial - super().__init__(*args, **kwargs) + super().__init__(*args, **kwargs) \ No newline at end of file diff --git a/netbox/tenancy/formset.py b/netbox/tenancy/formset.py new file mode 100644 index 000000000..feac6a9cc --- /dev/null +++ b/netbox/tenancy/formset.py @@ -0,0 +1,31 @@ +from django import forms +from utilities.forms import APISelectMultiple, FilterChoiceField +from .models import Tenant, TenantGroup + +# +# Tenancy filtering form extension +# +class TenancyFilterForm(forms.Form): + 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, + ) + ) \ No newline at end of file diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 06b8f8553..a3ae66f3d 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -6,7 +6,7 @@ from netaddr.core import AddrFormatError from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet -from tenancy.filters import TenancyFilterSet +from tenancy.filterset import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 911c7064a..2e881b184 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -7,7 +7,8 @@ from dcim.forms import INTERFACE_MODE_HELP_TEXT from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm from ipam.models import IPAddress -from tenancy.forms import TenancyForm, TenancyFilterForm +from tenancy.forms import TenancyForm +from tenancy.formset import TenancyFilterForm from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, From 6fa54bed73aaa27ed9307c6ddac3b8def4b36666 Mon Sep 17 00:00:00 2001 From: dansheps Date: Wed, 10 Apr 2019 08:42:27 -0500 Subject: [PATCH 07/43] Fix PEP8 Errors --- netbox/tenancy/forms.py | 2 +- netbox/tenancy/formset.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 6095b1f5b..e7e065ff7 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -152,4 +152,4 @@ class TenancyForm(ChainedFieldsMixin, forms.Form): initial['tenant_group'] = instance.tenant.group kwargs['initial'] = initial - super().__init__(*args, **kwargs) \ No newline at end of file + super().__init__(*args, **kwargs) diff --git a/netbox/tenancy/formset.py b/netbox/tenancy/formset.py index feac6a9cc..e07517bbe 100644 --- a/netbox/tenancy/formset.py +++ b/netbox/tenancy/formset.py @@ -2,6 +2,7 @@ from django import forms from utilities.forms import APISelectMultiple, FilterChoiceField from .models import Tenant, TenantGroup + # # Tenancy filtering form extension # @@ -28,4 +29,4 @@ class TenancyFilterForm(forms.Form): value_field="slug", null_option=True, ) - ) \ No newline at end of file + ) From 22e5834d8b5f42b8408dedcd1cbd9f696f1deb42 Mon Sep 17 00:00:00 2001 From: dansheps Date: Tue, 30 Apr 2019 10:06:27 -0500 Subject: [PATCH 08/43] Remove tenant group from ipam table --- netbox/ipam/tables.py | 6 +----- netbox/tenancy/tables.py | 10 ---------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 8d11d8f77..9578b4407 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -169,12 +169,8 @@ VLAN_MEMBER_ACTIONS = """ """ TENANT_LINK = """ -{% if record.tenant and record.tenant.group %} - {{record.tenant.group}}:{{ record.tenant }} -{% elif record.tenant %} +{% if record.tenant %} {{ record.tenant }} -{% elif record.vrf.tenant.group %} - {{record.vrf.tenant.group}}:{{ record.vrf.tenant }}* {% elif record.vrf.tenant %} {{ record.vrf.tenant }}* {% else %} diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 4bb8a4f77..884bdc3df 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -20,16 +20,6 @@ COL_TENANT = """ {% endif %} """ -COL_TENANTGROUP_TENANT = """ -{% if record.tenant and record.tenant.group %} - {{record.tenant.group}}:{{ record.tenant }} -{% elif record.tenant %} - {{ record.tenant }} -{% else %} - — -{% endif %} -""" - # # Tenant groups From ee4a3bcb0251b7f3d00dd9e4b001c190e81b0ae2 Mon Sep 17 00:00:00 2001 From: Shane Madden Date: Wed, 1 May 2019 13:47:52 -0600 Subject: [PATCH 09/43] Add circuittermination as a choice for cable endpoint types, which is not in the choices API for cable termination types but is accepted by the application as a valid endpoint for cables --- netbox/dcim/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 926b97130..9bd549541 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -360,7 +360,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 From 5f5e4ce1a13ad41401ef86e54a8f9786852865d1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 May 2019 11:41:37 -0400 Subject: [PATCH 10/43] Changelog for #3132 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82a4a7e41..1b3d827cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +2.5.13 (FUTURE) + +## Bug Fixes + +* [#3132](https://github.com/digitalocean/netbox/issues/3132) - Circuit termination missing from available cable termination types + +--- + 2.5.12 (2019-05-01) ## Bug Fixes From 01c5d9e9095339c386faf033efaffad480b10aa0 Mon Sep 17 00:00:00 2001 From: Austin English Date: Thu, 2 May 2019 13:02:53 -0500 Subject: [PATCH 11/43] upgrade.sh: make sure we are in the right directory --- upgrade.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/upgrade.sh b/upgrade.sh index 24e79f5bd..880d27d5d 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -5,6 +5,8 @@ # Once the script completes, remember to restart the WSGI service (e.g. # gunicorn or uWSGI). +cd "$(dirname "$0")" + PYTHON="python3" PIP="pip3" From 244c07e5f7fcc2ef16c839a55ba232437040463f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 May 2019 15:36:30 -0400 Subject: [PATCH 12/43] Closes #3085: Catch all exceptions during export template rendering --- CHANGELOG.md | 4 ++++ netbox/utilities/views.py | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b3d827cd..6662a046e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ 2.5.13 (FUTURE) +## Enhancements + +* [#3085](https://github.com/digitalocean/netbox/issues/3085) - Catch all exceptions during export template rendering + ## Bug Fixes * [#3132](https://github.com/digitalocean/netbox/issues/3132) - Circuit termination missing from available cable termination types diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index f52f4ea9e..8b92e5a59 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -126,10 +126,12 @@ class ObjectListView(View): queryset = CustomFieldQueryset(self.queryset, custom_fields) if custom_fields else self.queryset try: return et.render_to_response(queryset) - except TemplateSyntaxError: + except Exception as e: messages.error( request, - "There was an error rendering the selected export template ({}).".format(et.name) + "There was an error rendering the selected export template ({}): {}".format( + et.name, e + ) ) # Fall back to built-in CSV formatting if export requested but no template specified From 1b862045e3f933a867010ed87107c62b1c1e22dd Mon Sep 17 00:00:00 2001 From: Oli <932481+tb-killa@users.noreply.github.com> Date: Mon, 6 May 2019 15:36:44 +0200 Subject: [PATCH 13/43] Formatting of cable length in cable trace --- netbox/templates/dcim/cable_trace.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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

From b97339017bb3cc8ed2bfb404f7c4566bdacfbe8f Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Mon, 6 May 2019 11:54:16 -0500 Subject: [PATCH 14/43] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6662a046e..262c38c1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Enhancements +* [#3150](https://github.com/digitalocean/netbox/issues/3150) - Fix formatting of cable length during cable trace * [#3085](https://github.com/digitalocean/netbox/issues/3085) - Catch all exceptions during export template rendering ## Bug Fixes From fbde6282b218d956842ce509e7e5ae5d64181ac9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 9 May 2019 14:32:49 -0400 Subject: [PATCH 15/43] Cleanup from #2931 --- CHANGELOG.md | 1 + netbox/circuits/filters.py | 6 +- netbox/circuits/forms.py | 3 +- netbox/dcim/filters.py | 23 +++-- netbox/dcim/forms.py | 87 ++++++++++++------- netbox/extras/filters.py | 26 +++++- netbox/extras/forms.py | 24 +++-- netbox/ipam/filters.py | 12 +-- netbox/ipam/forms.py | 17 ++-- .../tenancy/{filterset.py => filtersets.py} | 0 netbox/tenancy/forms.py | 1 + netbox/virtualization/filters.py | 3 +- netbox/virtualization/forms.py | 12 +-- 13 files changed, 142 insertions(+), 73 deletions(-) rename netbox/tenancy/{filterset.py => filtersets.py} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 262c38c1a..a5ab7badb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Enhancements +* [#2813](https://github.com/digitalocean/netbox/issues/2813) - Add tenant group filters * [#3150](https://github.com/digitalocean/netbox/issues/3150) - Fix formatting of cable length during cable trace * [#3085](https://github.com/digitalocean/netbox/issues/3085) - Catch all exceptions during export template rendering diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index e79b78e7b..4decb7166 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -3,13 +3,13 @@ from django.db.models import Q from dcim.models import Site from extras.filters import CustomFieldFilterSet -from tenancy.filterset import TenancyFilterSet +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 -class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet): +class ProviderFilter(CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -54,7 +54,7 @@ class CircuitTypeFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, django_filters.FilterSet): +class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index f90cb9503..8e5fbf9f7 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -268,8 +268,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Circuit - # Order the form fields, fields not listed are appended - field_order = ['q', 'type', 'provider', 'status'] + field_order = ['q', 'type', 'provider', 'status', 'site', 'tenant_group', 'tenant', 'commit_rate'] q = forms.CharField( required=False, label='Search' diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 47f052f70..f9f0a57d6 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,13 +1,13 @@ import django_filters from django.contrib.auth.models import User -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from netaddr import EUI from netaddr.core import AddrFormatError from extras.filters import CustomFieldFilterSet -from tenancy.filterset import TenancyFilterSet +from tenancy.filtersets import TenancyFilterSet +from tenancy.models import Tenant from utilities.constants import COLOR_CHOICES from utilities.filters import ( NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter @@ -39,7 +39,7 @@ class RegionFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): +class SiteFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -114,7 +114,7 @@ class RackRoleFilter(NameSlugSearchFilterSet): fields = ['name', 'slug', 'color'] -class RackFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): +class RackFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -180,7 +180,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSe ) -class RackReservationFilter(TenancyFilterSet, django_filters.FilterSet): +class RackReservationFilter(TenancyFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -875,7 +875,7 @@ class InventoryItemFilter(DeviceComponentFilterSet): return queryset.filter(qs_filter) -class VirtualChassisFilter(TenancyFilterSet, django_filters.FilterSet): +class VirtualChassisFilter(django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -891,6 +891,17 @@ class VirtualChassisFilter(TenancyFilterSet, django_filters.FilterSet): to_field_name='slug', label='Site name (slug)', ) + tenant_id = django_filters.ModelMultipleChoiceFilter( + field_name='master__tenant', + queryset=Tenant.objects.all(), + label='Tenant (ID)', + ) + tenant = django_filters.ModelMultipleChoiceFilter( + field_name='master__tenant__slug', + queryset=Tenant.objects.all(), + to_field_name='slug', + label='Tenant (slug)', + ) tag = TagFilter() class Meta: diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 56a1cb9b4..369ebeb23 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -14,7 +14,7 @@ from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEdit from ipam.models import IPAddress, VLAN, VLANGroup from tenancy.forms import TenancyForm from tenancy.formset import TenancyFilterForm -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, @@ -259,8 +259,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Site - # Order the form fields, fields not listed are appended - field_order = ['q', 'status', 'region'] + field_order = ['q', 'status', 'region', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' @@ -591,8 +590,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Rack - # Order the form fields, fields not listed are appended - field_order = ['q', 'site', 'group_id'] + field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' @@ -674,32 +672,6 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm): return unit_choices -class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, forms.Form): - # Order the form fields, fields not listed are appended - field_order = ['q', 'site', 'group_id'] - 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, - ) - ) - - class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RackReservation.objects.all(), @@ -728,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 # @@ -1622,8 +1619,10 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF class DeviceFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Device - # Order the form fields, fields not listed are appended - field_order = ['q', 'region', 'site', 'rack_group_id', 'rack_id', 'role'] + 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' @@ -3074,9 +3073,31 @@ 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/extras/filters.py b/netbox/extras/filters.py index 712dfd6d8..d5457a5a6 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -4,7 +4,7 @@ from django.db.models import Q from taggit.models import Tag from dcim.models import DeviceRole, Platform, Region, Site -from tenancy.filterset import TenancyFilterSet +from tenancy.models import Tenant, TenantGroup from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap @@ -122,7 +122,7 @@ class TopologyMapFilter(django_filters.FilterSet): fields = ['name', 'slug'] -class ConfigContextFilter(TenancyFilterSet, django_filters.FilterSet): +class ConfigContextFilter(django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -171,6 +171,28 @@ class ConfigContextFilter(TenancyFilterSet, django_filters.FilterSet): to_field_name='slug', label='Platform (slug)', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant_groups', + queryset=TenantGroup.objects.all(), + label='Tenant group', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant_groups__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant group (slug)', + ) + tenant_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenants', + queryset=Tenant.objects.all(), + label='Tenant', + ) + tenant = django_filters.ModelMultipleChoiceFilter( + field_name='tenants__slug', + queryset=Tenant.objects.all(), + to_field_name='slug', + label='Tenant (slug)', + ) class Meta: model = ConfigContext diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index a956597d3..54eee0c5c 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -8,7 +8,7 @@ from taggit.forms import TagField from taggit.models import Tag from dcim.models import DeviceRole, Platform, Region, Site -from tenancy.formset import TenancyFilterForm +from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, @@ -274,7 +274,7 @@ class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm): ] -class ConfigContextFilterForm(TenancyFilterForm, BootstrapMixin, forms.Form): +class ConfigContextFilterForm(BootstrapMixin, forms.Form): q = forms.CharField( required=False, label='Search' @@ -311,10 +311,22 @@ class ConfigContextFilterForm(TenancyFilterForm, BootstrapMixin, forms.Form): value_field="slug", ) ) - - class Meta: - # Order the form fields, fields not listed are appended - field_order = ['q', 'type', 'provider', 'status'] + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + ) + ) + tenant = FilterChoiceField( + queryset=Tenant.objects.all(), + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/tenancy/tenants/", + value_field="slug", + ) + ) # diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 3050a7901..a6e986117 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.filterset import TenancyFilterSet +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(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): +class VRFFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -49,7 +49,7 @@ class RIRFilter(NameSlugSearchFilterSet): fields = ['name', 'slug', 'is_private'] -class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet): +class AggregateFilter(CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -97,7 +97,7 @@ class RoleFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): +class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -234,7 +234,7 @@ class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.Filter return queryset.filter(prefix__net_mask_length=value) -class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): +class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -364,7 +364,7 @@ class VLANGroupFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class VLANFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): +class VLANFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 1bc83a9d0..a8e9df6c5 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -100,8 +100,7 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VRF - # Order the form fields, fields not listed are appended - field_order = ['q'] + field_order = ['q', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' @@ -492,8 +491,10 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Prefix - # Order the form fields, fields not listed are appended - field_order = ['q', 'within_include', 'family', 'mask_length', 'vrf'] + 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' @@ -931,8 +932,9 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form): class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = IPAddress - # Order the form fields, fields not listed are appended - field_order = ['q', 'parent', 'family', 'mask_length', 'vrf'] + field_order = [ + 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'tenant_group', 'tenant', + ] q = forms.CharField( required=False, label='Search' @@ -1200,8 +1202,7 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VLAN - # Order the form fields, fields not listed are appended - field_order = ['q', 'site', 'group_id'] + field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' diff --git a/netbox/tenancy/filterset.py b/netbox/tenancy/filtersets.py similarity index 100% rename from netbox/tenancy/filterset.py rename to netbox/tenancy/filtersets.py diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index e7e065ff7..636cfed03 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -118,6 +118,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): # # Tenancy form extension # + class TenancyForm(ChainedFieldsMixin, forms.Form): tenant_group = forms.ModelChoiceField( queryset=TenantGroup.objects.all(), diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index a3ae66f3d..e71693ac6 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -1,12 +1,11 @@ import django_filters -from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from netaddr import EUI from netaddr.core import AddrFormatError from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet -from tenancy.filterset import TenancyFilterSet +from tenancy.filtersets import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 2e881b184..e61c0e320 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -9,7 +9,7 @@ from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomField from ipam.models import IPAddress from tenancy.forms import TenancyForm from tenancy.formset import TenancyFilterForm -from tenancy.models import Tenant, TenantGroup +from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm, @@ -337,8 +337,8 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): class Meta: model = VirtualMachine fields = [ - 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', - 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data', + 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', + 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data', ] help_texts = { 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered " @@ -523,8 +523,10 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VirtualMachine - # Order the form fields, fields not listed are appended - field_order = ['q', 'cluster_group', 'cluster_type', 'cluster_id', 'region', 'site'] + field_order = [ + 'q', 'cluster_group', 'cluster_type', 'cluster_id', 'status', 'role', 'region', 'site', 'tenant_group', + 'tenant', 'platform', + ] q = forms.CharField( required=False, label='Search' From e19feb92eaa9a0f297e15d79b41597eeb2b6a415 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 9 May 2019 14:36:18 -0400 Subject: [PATCH 16/43] Move TenancyFilterForm to tenancy.forms --- netbox/circuits/forms.py | 2 +- netbox/dcim/forms.py | 2 +- netbox/ipam/forms.py | 2 +- netbox/tenancy/forms.py | 28 +++++++++++++++++++++++++++- netbox/tenancy/formset.py | 32 -------------------------------- netbox/virtualization/forms.py | 2 +- 6 files changed, 31 insertions(+), 37 deletions(-) delete mode 100644 netbox/tenancy/formset.py diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 8e5fbf9f7..100c6334f 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -4,7 +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.formset import TenancyFilterForm +from tenancy.forms import TenancyFilterForm from tenancy.models import Tenant from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 369ebeb23..f10418d57 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -13,7 +13,7 @@ from timezone_field import TimeZoneFormField from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from ipam.models import IPAddress, VLAN, VLANGroup from tenancy.forms import TenancyForm -from tenancy.formset import TenancyFilterForm +from tenancy.forms import TenancyFilterForm from tenancy.models import Tenant, TenantGroup from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index a8e9df6c5..d7c0b1a0c 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -6,7 +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.formset import TenancyFilterForm +from tenancy.forms import TenancyFilterForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 636cfed03..f8aaa45e5 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -116,7 +116,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): # -# Tenancy form extension +# Form extensions # class TenancyForm(ChainedFieldsMixin, forms.Form): @@ -154,3 +154,29 @@ class TenancyForm(ChainedFieldsMixin, forms.Form): kwargs['initial'] = initial super().__init__(*args, **kwargs) + + +class TenancyFilterForm(forms.Form): + 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/tenancy/formset.py b/netbox/tenancy/formset.py deleted file mode 100644 index e07517bbe..000000000 --- a/netbox/tenancy/formset.py +++ /dev/null @@ -1,32 +0,0 @@ -from django import forms -from utilities.forms import APISelectMultiple, FilterChoiceField -from .models import Tenant, TenantGroup - - -# -# Tenancy filtering form extension -# -class TenancyFilterForm(forms.Form): - 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/virtualization/forms.py b/netbox/virtualization/forms.py index e61c0e320..59b340e85 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -8,7 +8,7 @@ from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, S from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm from ipam.models import IPAddress from tenancy.forms import TenancyForm -from tenancy.formset import TenancyFilterForm +from tenancy.forms import TenancyFilterForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, From 62d497dd0bf7f829c9c1dc6b28e63b6455e2a871 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 14 May 2019 19:03:03 -0400 Subject: [PATCH 17/43] Closes #3186: Add interface name filter for IP addresses --- CHANGELOG.md | 1 + netbox/ipam/filters.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5ab7badb..6014da3dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * [#2813](https://github.com/digitalocean/netbox/issues/2813) - Add tenant group filters * [#3150](https://github.com/digitalocean/netbox/issues/3150) - Fix formatting of cable length during cable trace * [#3085](https://github.com/digitalocean/netbox/issues/3085) - Catch all exceptions during export template rendering +* [#3186](https://github.com/digitalocean/netbox/issues/3186) - Add interface name filter for IP addresses ## Bug Fixes diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index a6e986117..a5464e4d0 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -286,6 +286,12 @@ class IPAddressFilter(TenancyFilterSet, 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)', From 2f32488c25610d3e6c4db2c8731fac58b655859e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 May 2019 19:45:36 -0400 Subject: [PATCH 18/43] Fixes #3190: Fix custom field rendering for Jinja2 export templates --- CHANGELOG.md | 1 + netbox/extras/models.py | 1 + netbox/extras/querysets.py | 18 ++++++++++++++++++ netbox/utilities/views.py | 26 +++++--------------------- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6014da3dc..a63cd9f87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * [#3150](https://github.com/digitalocean/netbox/issues/3150) - Fix formatting of cable length during cable trace * [#3085](https://github.com/digitalocean/netbox/issues/3085) - Catch all exceptions during export template rendering * [#3186](https://github.com/digitalocean/netbox/issues/3186) - Add interface name filter for IP addresses +* [#3190](https://github.com/digitalocean/netbox/issues/3190) - Fix custom field rendering for Jinja2 export templates ## Bug Fixes diff --git a/netbox/extras/models.py b/netbox/extras/models.py index da8f09a50..4c1f9cb76 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -106,6 +106,7 @@ class CustomFieldModel(models.Model): class Meta: abstract = True + @property def cf(self): """ Name-based CustomFieldValue accessor for use in templates 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/utilities/views.py b/netbox/utilities/views.py index 8b92e5a59..fd54088ba 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -1,5 +1,4 @@ import sys -from collections import OrderedDict from copy import deepcopy from django.conf import settings @@ -12,7 +11,7 @@ from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHidd from django.http import HttpResponse, HttpResponseServerError from django.shortcuts import get_object_or_404, redirect, render from django.template import loader -from django.template.exceptions import TemplateDoesNotExist, TemplateSyntaxError +from django.template.exceptions import TemplateDoesNotExist from django.urls import reverse from django.utils.html import escape from django.utils.http import is_safe_url @@ -23,6 +22,7 @@ from django.views.generic import View from django_tables2 import RequestConfig from extras.models import CustomField, CustomFieldValue, ExportTemplate +from extras.querysets import CustomFieldQueryset from utilities.forms import BootstrapMixin, CSVDataField from utilities.utils import csv_format from .error_handlers import handle_protectederror @@ -30,23 +30,6 @@ from .forms import ConfirmationForm from .paginator import EnhancedPaginator -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 GetReturnURLMixin(object): """ Provides logic for determining where a user should be redirected after processing a form. @@ -115,8 +98,9 @@ class ObjectListView(View): self.queryset = self.filter(request.GET, self.queryset).qs # If this type of object has one or more custom fields, prefetch any relevant custom field values - custom_fields = CustomField.objects.filter(obj_type=ContentType.objects.get_for_model(model))\ - .prefetch_related('choices') + custom_fields = CustomField.objects.filter( + obj_type=ContentType.objects.get_for_model(model) + ).prefetch_related('choices') if custom_fields: self.queryset = self.queryset.prefetch_related('custom_field_values') From 9b47e57e8e6af58ab324772d04c162f1a9daf020 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 May 2019 20:24:55 -0400 Subject: [PATCH 19/43] Closes #3183: Enable bulk deletion of sites --- CHANGELOG.md | 5 +++-- netbox/dcim/urls.py | 1 + netbox/dcim/views.py | 8 ++++++++ netbox/templates/dcim/site_list.html | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a63cd9f87..44c6bc45d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,14 +3,15 @@ ## Enhancements * [#2813](https://github.com/digitalocean/netbox/issues/2813) - Add tenant group filters -* [#3150](https://github.com/digitalocean/netbox/issues/3150) - Fix formatting of cable length during cable trace * [#3085](https://github.com/digitalocean/netbox/issues/3085) - Catch all exceptions during export template rendering +* [#3183](https://github.com/digitalocean/netbox/issues/3183) - Enable bulk deletion of sites * [#3186](https://github.com/digitalocean/netbox/issues/3186) - Add interface name filter for IP addresses -* [#3190](https://github.com/digitalocean/netbox/issues/3190) - Fix custom field rendering for Jinja2 export templates ## Bug Fixes * [#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 +* [#3190](https://github.com/digitalocean/netbox/issues/3190) - Fix custom field rendering for Jinja2 export templates --- diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 21d620af1..07cf4b010 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -25,6 +25,7 @@ urlpatterns = [ 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/delete/$', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'), 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'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 27f90a3a2..85d37b29f 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -247,6 +247,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 # 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' %} From edabc8eee96313427605de63ab30e5f62e46975d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 May 2019 20:49:00 -0400 Subject: [PATCH 20/43] Closes #3138: Add 2.5GE and 5GE interface form factors --- CHANGELOG.md | 1 + netbox/dcim/constants.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44c6bc45d..124b60a95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * [#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 * [#3183](https://github.com/digitalocean/netbox/issues/3183) - Enable bulk deletion of sites * [#3186](https://github.com/digitalocean/netbox/issues/3186) - Add interface name filter for IP addresses diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 9bd549541..ea7bded2b 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -75,6 +75,8 @@ IFACE_FF_100ME_FIXED = 800 IFACE_FF_1GE_FIXED = 1000 IFACE_FF_1GE_GBIC = 1050 IFACE_FF_1GE_SFP = 1100 +IFACE_FF_2GE_FIXED = 1120 +IFACE_FF_5GE_FIXED = 1130 IFACE_FF_10GE_FIXED = 1150 IFACE_FF_10GE_CX4 = 1170 IFACE_FF_10GE_SFP_PLUS = 1200 @@ -150,6 +152,8 @@ IFACE_FF_CHOICES = [ [ [IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'], [IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'], + [IFACE_FF_2GE_FIXED, '2.5GBASE-T (2.5GE)'], + [IFACE_FF_5GE_FIXED, '5GBASE-T (5GE)'], [IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'], [IFACE_FF_10GE_CX4, '10GBASE-CX4 (10GE)'], ] From cbace6f83178fe49f6d52bda428a4868fcd749ad Mon Sep 17 00:00:00 2001 From: Khaled BEN ABDALLAH Date: Sat, 18 May 2019 21:06:22 +0200 Subject: [PATCH 21/43] Fixes #3031: Select2 creates multiple tags for tags with spaces --- netbox/project-static/js/forms.js | 4 ++++ 1 file changed, 4 insertions(+) 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 From 4313a717c4111c0dd34465375b8795e0ef7b7db5 Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Mon, 20 May 2019 21:06:53 +0100 Subject: [PATCH 22/43] Add grey border around color-block Fixes #3184 --- netbox/project-static/css/base.css | 1 + 1 file changed, 1 insertion(+) 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; From f9cd89a4a454e91f721e5c3f49cda29ebc41e578 Mon Sep 17 00:00:00 2001 From: hellerve Date: Sun, 26 May 2019 14:38:27 +0200 Subject: [PATCH 23/43] urls: fix 3168 by changing url to path --- netbox/circuits/urls.py | 62 ++--- netbox/dcim/urls.py | 414 +++++++++++++++++----------------- netbox/extras/urls.py | 40 ++-- netbox/ipam/urls.py | 154 ++++++------- netbox/netbox/urls.py | 57 ++--- netbox/secrets/urls.py | 30 +-- netbox/tenancy/urls.py | 32 +-- netbox/users/urls.py | 20 +- netbox/virtualization/urls.py | 82 +++---- 9 files changed, 446 insertions(+), 445 deletions(-) diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index be1106308..440960d64 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,41 @@ 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/$', 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/urls.py b/netbox/dcim/urls.py index 07cf4b010..25c3a5a4d 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 @@ -13,271 +13,271 @@ 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/delete/$', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'), - 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/$', 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/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'), - url(r'^console-server-ports/(?P\d+)/connect/$', 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/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/$', 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/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), - url(r'^power-outlets/(?P\d+)/connect/$', 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/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/$', 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/$', 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/$', 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'), ] diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 35a6fb110..5ba7c110c 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 @@ -6,32 +6,32 @@ 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'), + 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'), # 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/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 45c99beb9..ad3806c6f 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.urls import path from django.views.static import serve from drf_yasg import openapi from drf_yasg.views import get_schema_view @@ -24,58 +25,58 @@ 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'), + url(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)), ] # 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/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/tenancy/urls.py b/netbox/tenancy/urls.py index 19522e6c7..fb23a6ef1 100644 --- a/netbox/tenancy/urls.py +++ b/netbox/tenancy/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,22 +8,22 @@ app_name = 'tenancy' urlpatterns = [ # Tenant groups - url(r'^tenant-groups/$', views.TenantGroupListView.as_view(), name='tenantgroup_list'), - url(r'^tenant-groups/add/$', views.TenantGroupCreateView.as_view(), name='tenantgroup_add'), - url(r'^tenant-groups/import/$', views.TenantGroupBulkImportView.as_view(), name='tenantgroup_import'), - url(r'^tenant-groups/delete/$', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'), - url(r'^tenant-groups/(?P[\w-]+)/edit/$', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'), - url(r'^tenant-groups/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='tenantgroup_changelog', kwargs={'model': TenantGroup}), + path(r'tenant-groups/', views.TenantGroupListView.as_view(), name='tenantgroup_list'), + path(r'tenant-groups/add/', views.TenantGroupCreateView.as_view(), name='tenantgroup_add'), + path(r'tenant-groups/import/', views.TenantGroupBulkImportView.as_view(), name='tenantgroup_import'), + path(r'tenant-groups/delete/', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'), + path(r'tenant-groups//edit/', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'), + path(r'tenant-groups//changelog/', ObjectChangeLogView.as_view(), name='tenantgroup_changelog', kwargs={'model': TenantGroup}), # Tenants - url(r'^tenants/$', views.TenantListView.as_view(), name='tenant_list'), - url(r'^tenants/add/$', views.TenantCreateView.as_view(), name='tenant_add'), - url(r'^tenants/import/$', views.TenantBulkImportView.as_view(), name='tenant_import'), - url(r'^tenants/edit/$', views.TenantBulkEditView.as_view(), name='tenant_bulk_edit'), - url(r'^tenants/delete/$', views.TenantBulkDeleteView.as_view(), name='tenant_bulk_delete'), - url(r'^tenants/(?P[\w-]+)/$', views.TenantView.as_view(), name='tenant'), - url(r'^tenants/(?P[\w-]+)/edit/$', views.TenantEditView.as_view(), name='tenant_edit'), - url(r'^tenants/(?P[\w-]+)/delete/$', views.TenantDeleteView.as_view(), name='tenant_delete'), - url(r'^tenants/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='tenant_changelog', kwargs={'model': Tenant}), + path(r'tenants/', views.TenantListView.as_view(), name='tenant_list'), + path(r'tenants/add/', views.TenantCreateView.as_view(), name='tenant_add'), + path(r'tenants/import/', views.TenantBulkImportView.as_view(), name='tenant_import'), + path(r'tenants/edit/', views.TenantBulkEditView.as_view(), name='tenant_bulk_edit'), + path(r'tenants/delete/', views.TenantBulkDeleteView.as_view(), name='tenant_bulk_delete'), + path(r'tenants//', views.TenantView.as_view(), name='tenant'), + path(r'tenants//edit/', views.TenantEditView.as_view(), name='tenant_edit'), + path(r'tenants//delete/', views.TenantDeleteView.as_view(), name='tenant_delete'), + path(r'tenants//changelog/', ObjectChangeLogView.as_view(), name='tenant_changelog', kwargs={'model': Tenant}), ] diff --git a/netbox/users/urls.py b/netbox/users/urls.py index a45f859e7..40fdbeab1 100644 --- a/netbox/users/urls.py +++ b/netbox/users/urls.py @@ -1,18 +1,18 @@ -from django.conf.urls import url +from django.urls import path from . import views app_name = 'user' urlpatterns = [ - url(r'^profile/$', views.ProfileView.as_view(), name='profile'), - url(r'^password/$', views.ChangePasswordView.as_view(), name='change_password'), - url(r'^api-tokens/$', views.TokenListView.as_view(), name='token_list'), - url(r'^api-tokens/add/$', views.TokenEditView.as_view(), name='token_add'), - url(r'^api-tokens/(?P\d+)/edit/$', views.TokenEditView.as_view(), name='token_edit'), - url(r'^api-tokens/(?P\d+)/delete/$', views.TokenDeleteView.as_view(), name='token_delete'), - url(r'^user-key/$', views.UserKeyView.as_view(), name='userkey'), - url(r'^user-key/edit/$', views.UserKeyEditView.as_view(), name='userkey_edit'), - url(r'^session-key/delete/$', views.SessionKeyDeleteView.as_view(), name='sessionkey_delete'), + path(r'profile/', views.ProfileView.as_view(), name='profile'), + path(r'password/', views.ChangePasswordView.as_view(), name='change_password'), + path(r'api-tokens/', views.TokenListView.as_view(), name='token_list'), + path(r'api-tokens/add/', views.TokenEditView.as_view(), name='token_add'), + path(r'api-tokens//edit/', views.TokenEditView.as_view(), name='token_edit'), + path(r'api-tokens//delete/', views.TokenDeleteView.as_view(), name='token_delete'), + path(r'user-key/', views.UserKeyView.as_view(), name='userkey'), + path(r'user-key/edit/', views.UserKeyEditView.as_view(), name='userkey_edit'), + path(r'session-key/delete/', views.SessionKeyDeleteView.as_view(), name='sessionkey_delete'), ] diff --git a/netbox/virtualization/urls.py b/netbox/virtualization/urls.py index 5fc5997a8..7cc28be51 100644 --- a/netbox/virtualization/urls.py +++ b/netbox/virtualization/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from extras.views import ObjectChangeLogView from ipam.views import ServiceCreateView @@ -9,53 +9,53 @@ app_name = 'virtualization' urlpatterns = [ # Cluster types - url(r'^cluster-types/$', views.ClusterTypeListView.as_view(), name='clustertype_list'), - url(r'^cluster-types/add/$', views.ClusterTypeCreateView.as_view(), name='clustertype_add'), - url(r'^cluster-types/import/$', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'), - url(r'^cluster-types/delete/$', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'), - url(r'^cluster-types/(?P[\w-]+)/edit/$', views.ClusterTypeEditView.as_view(), name='clustertype_edit'), - url(r'^cluster-types/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}), + path(r'cluster-types/', views.ClusterTypeListView.as_view(), name='clustertype_list'), + path(r'cluster-types/add/', views.ClusterTypeCreateView.as_view(), name='clustertype_add'), + path(r'cluster-types/import/', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'), + path(r'cluster-types/delete/', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'), + path(r'cluster-types//edit/', views.ClusterTypeEditView.as_view(), name='clustertype_edit'), + path(r'cluster-types//changelog/', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}), # Cluster groups - url(r'^cluster-groups/$', views.ClusterGroupListView.as_view(), name='clustergroup_list'), - url(r'^cluster-groups/add/$', views.ClusterGroupCreateView.as_view(), name='clustergroup_add'), - url(r'^cluster-groups/import/$', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'), - url(r'^cluster-groups/delete/$', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'), - url(r'^cluster-groups/(?P[\w-]+)/edit/$', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'), - url(r'^cluster-groups/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}), + path(r'cluster-groups/', views.ClusterGroupListView.as_view(), name='clustergroup_list'), + path(r'cluster-groups/add/', views.ClusterGroupCreateView.as_view(), name='clustergroup_add'), + path(r'cluster-groups/import/', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'), + path(r'cluster-groups/delete/', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'), + path(r'cluster-groups//edit/', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'), + path(r'cluster-groups//changelog/', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}), # Clusters - url(r'^clusters/$', views.ClusterListView.as_view(), name='cluster_list'), - url(r'^clusters/add/$', views.ClusterCreateView.as_view(), name='cluster_add'), - url(r'^clusters/import/$', views.ClusterBulkImportView.as_view(), name='cluster_import'), - url(r'^clusters/edit/$', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'), - url(r'^clusters/delete/$', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'), - url(r'^clusters/(?P\d+)/$', views.ClusterView.as_view(), name='cluster'), - url(r'^clusters/(?P\d+)/edit/$', views.ClusterEditView.as_view(), name='cluster_edit'), - url(r'^clusters/(?P\d+)/delete/$', views.ClusterDeleteView.as_view(), name='cluster_delete'), - url(r'^clusters/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}), - url(r'^clusters/(?P\d+)/devices/add/$', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'), - url(r'^clusters/(?P\d+)/devices/remove/$', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'), + path(r'clusters/', views.ClusterListView.as_view(), name='cluster_list'), + path(r'clusters/add/', views.ClusterCreateView.as_view(), name='cluster_add'), + path(r'clusters/import/', views.ClusterBulkImportView.as_view(), name='cluster_import'), + path(r'clusters/edit/', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'), + path(r'clusters/delete/', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'), + path(r'clusters//', views.ClusterView.as_view(), name='cluster'), + path(r'clusters//edit/', views.ClusterEditView.as_view(), name='cluster_edit'), + path(r'clusters//delete/', views.ClusterDeleteView.as_view(), name='cluster_delete'), + path(r'clusters//changelog/', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}), + path(r'clusters//devices/add/', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'), + path(r'clusters//devices/remove/', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'), # Virtual machines - url(r'^virtual-machines/$', views.VirtualMachineListView.as_view(), name='virtualmachine_list'), - url(r'^virtual-machines/add/$', views.VirtualMachineCreateView.as_view(), name='virtualmachine_add'), - url(r'^virtual-machines/import/$', views.VirtualMachineBulkImportView.as_view(), name='virtualmachine_import'), - url(r'^virtual-machines/edit/$', views.VirtualMachineBulkEditView.as_view(), name='virtualmachine_bulk_edit'), - url(r'^virtual-machines/delete/$', views.VirtualMachineBulkDeleteView.as_view(), name='virtualmachine_bulk_delete'), - url(r'^virtual-machines/(?P\d+)/$', views.VirtualMachineView.as_view(), name='virtualmachine'), - url(r'^virtual-machines/(?P\d+)/edit/$', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'), - url(r'^virtual-machines/(?P\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'), - url(r'^virtual-machines/(?P\d+)/config-context/$', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'), - url(r'^virtual-machines/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}), - url(r'^virtual-machines/(?P\d+)/services/assign/$', ServiceCreateView.as_view(), name='virtualmachine_service_assign'), + path(r'virtual-machines/', views.VirtualMachineListView.as_view(), name='virtualmachine_list'), + path(r'virtual-machines/add/', views.VirtualMachineCreateView.as_view(), name='virtualmachine_add'), + path(r'virtual-machines/import/', views.VirtualMachineBulkImportView.as_view(), name='virtualmachine_import'), + path(r'virtual-machines/edit/', views.VirtualMachineBulkEditView.as_view(), name='virtualmachine_bulk_edit'), + path(r'virtual-machines/delete/', views.VirtualMachineBulkDeleteView.as_view(), name='virtualmachine_bulk_delete'), + path(r'virtual-machines//', views.VirtualMachineView.as_view(), name='virtualmachine'), + path(r'virtual-machines//edit/', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'), + path(r'virtual-machines//delete/', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'), + path(r'virtual-machines//config-context/', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'), + path(r'virtual-machines//changelog/', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}), + path(r'virtual-machines//services/assign/', ServiceCreateView.as_view(), name='virtualmachine_service_assign'), # VM interfaces - url(r'^virtual-machines/interfaces/add/$', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_interface'), - url(r'^virtual-machines/(?P\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'), - url(r'^virtual-machines/(?P\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), - url(r'^virtual-machines/(?P\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), - url(r'^vm-interfaces/(?P\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'), - url(r'^vm-interfaces/(?P\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'), + path(r'virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_interface'), + path(r'virtual-machines//interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'), + path(r'virtual-machines//interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), + path(r'virtual-machines//interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), + path(r'vm-interfaces//edit/', views.InterfaceEditView.as_view(), name='interface_edit'), + path(r'vm-interfaces//delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), ] From b114b9d39687d143c915ee21bb5673fb7077f173 Mon Sep 17 00:00:00 2001 From: hellerve Date: Sun, 26 May 2019 14:44:28 +0200 Subject: [PATCH 24/43] utilities: add converters module and use for json/yaml url --- netbox/netbox/urls.py | 9 ++++++--- netbox/utilities/converters.py | 8 ++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 netbox/utilities/converters.py diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index ad3806c6f..6a1db6dea 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -1,14 +1,17 @@ from django.conf import settings -from django.conf.urls import include, url -from django.urls import path +from django.conf.urls import include +from django.urls import path, register_converter from django.views.static import serve from drf_yasg import openapi from drf_yasg.views import get_schema_view from netbox.views import APIRootView, HomeView, SearchView from users.views import LoginView, LogoutView +from utilities import converters from .admin import admin_site +register_converter(converters.JSONOrYAMLConverter, 'json_or_yaml') + schema_view = get_schema_view( openapi.Info( title="NetBox API", @@ -53,7 +56,7 @@ _patterns = [ 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'), - url(r'api/swagger(?P.json|.yaml)', schema_view.without_ui(), name='schema_swagger'), + path(r'api/swagger', schema_view.without_ui(), name='schema_swagger'), # Serving static media in Django to pipe it through LoginRequiredMiddleware path(r'media/', serve, {'document_root': settings.MEDIA_ROOT}), diff --git a/netbox/utilities/converters.py b/netbox/utilities/converters.py new file mode 100644 index 000000000..d8b8f69c4 --- /dev/null +++ b/netbox/utilities/converters.py @@ -0,0 +1,8 @@ +class JSONOrYAMLConverter: + regex = '.json|.yaml' + + def to_python(self, value): + return value + + def to_url(self, value): + return value From 473dafc2c88171f86c8497c751df55f20e0c6275 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 May 2019 15:03:00 -0400 Subject: [PATCH 25/43] Changelog for #3184 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 124b60a95..b6ec83534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * [#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 --- From 1366730a3fb43e652b894d47629fbac665db1574 Mon Sep 17 00:00:00 2001 From: hellerve Date: Mon, 27 May 2019 22:41:10 +0200 Subject: [PATCH 26/43] netbox urls: move to re_path as suggested by @jeremystretch --- netbox/netbox/urls.py | 7 ++----- netbox/utilities/converters.py | 8 -------- 2 files changed, 2 insertions(+), 13 deletions(-) delete mode 100644 netbox/utilities/converters.py diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 6a1db6dea..efcd17a87 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -1,17 +1,14 @@ from django.conf import settings from django.conf.urls import include -from django.urls import path, register_converter +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 from netbox.views import APIRootView, HomeView, SearchView from users.views import LoginView, LogoutView -from utilities import converters from .admin import admin_site -register_converter(converters.JSONOrYAMLConverter, 'json_or_yaml') - schema_view = get_schema_view( openapi.Info( title="NetBox API", @@ -56,7 +53,7 @@ _patterns = [ 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'), - path(r'api/swagger', schema_view.without_ui(), name='schema_swagger'), + 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 path(r'media/', serve, {'document_root': settings.MEDIA_ROOT}), diff --git a/netbox/utilities/converters.py b/netbox/utilities/converters.py deleted file mode 100644 index d8b8f69c4..000000000 --- a/netbox/utilities/converters.py +++ /dev/null @@ -1,8 +0,0 @@ -class JSONOrYAMLConverter: - regex = '.json|.yaml' - - def to_python(self, value): - return value - - def to_url(self, value): - return value From cc87d9901785f252d011f486eb95ecf74898dc1a Mon Sep 17 00:00:00 2001 From: hellerve Date: Tue, 28 May 2019 16:11:49 +0200 Subject: [PATCH 27/43] all: fix error message on trying to delete protected models (references #3211) --- netbox/ipam/tests/test_api.py | 18 ++++++++++++++++++ netbox/utilities/middleware.py | 9 +++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 3f4555b55..991f098bb 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, 403) + + content = json.loads(response.content.decode('utf-8')) + self.assertIn('detail', content) + self.assertTrue(content['detail'].startswith('You tried deleting a model that is protected by:')) + class ServiceTest(APITestCase): diff --git a/netbox/utilities/middleware.py b/netbox/utilities/middleware.py index 4e321ab19..6be01127c 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -1,6 +1,7 @@ from django.conf import settings from django.db import ProgrammingError -from django.http import Http404, HttpResponseRedirect +from django.http import Http404, HttpResponseRedirect, JsonResponse +from django.db.models import ProtectedError from django.urls import reverse from .views import server_error @@ -62,6 +63,11 @@ class ExceptionHandlingMiddleware(object): if isinstance(exception, Http404): return + elif isinstance(exception, ProtectedError): + models = '\n'.join('- {} ({})'.format(o, o._meta) for o in exception.protected_objects.all()) + msg = 'You tried deleting a model that is protected by:\n{}'.format(models) + return JsonResponse({'detail': msg}, status=403) + # Determine the type of exception. If it's a common issue, return a custom error page with instructions. custom_template = None if isinstance(exception, ProgrammingError): @@ -70,7 +76,6 @@ class ExceptionHandlingMiddleware(object): custom_template = 'exceptions/import_error.html' elif isinstance(exception, PermissionError): custom_template = 'exceptions/permission_error.html' - # Return a custom error message, or fall back to Django's default 500 error handling if custom_template: return server_error(request, template_name=custom_template) From 87f5dd05f5707f40f5987612297a0ce87c3c8af3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 May 2019 13:10:54 -0400 Subject: [PATCH 28/43] Fixes #3223: Fix filtering devices by "has power outlets" --- CHANGELOG.md | 1 + netbox/dcim/filters.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6ec83534..72aad08d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * [#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 +* [#3223](https://github.com/digitalocean/netbox/issues/3223) - Fix filtering devices by "has power outlets" --- diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index f9f0a57d6..ec1e03983 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -606,7 +606,7 @@ class DeviceFilter(TenancyFilterSet, 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) From c4f481d705a5a28722f6d58fe5909649bdfee6ca Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 May 2019 14:21:36 -0400 Subject: [PATCH 29/43] Bump DRF to 3.9.1 to address WS-2019-0037 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0bc96db8d..b69ce9475 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ django-tables2==2.0.3 django-taggit==0.23.0 django-taggit-serializer==0.1.7 django-timezone-field==3.0 -djangorestframework==3.9.0 +djangorestframework==3.9.1 drf-yasg[validation]==1.14.0 graphviz==0.10.1 Jinja2==2.10 From 2c7bad9fff4b4741f69c68586dbb624757221805 Mon Sep 17 00:00:00 2001 From: hellerve Date: Tue, 28 May 2019 21:11:23 +0200 Subject: [PATCH 30/43] utilities: move protectederror handling to modelviewset --- netbox/ipam/tests/test_api.py | 2 +- netbox/utilities/api.py | 18 +++++++++++++++++- netbox/utilities/middleware.py | 8 +------- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 991f098bb..0dad73550 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -972,7 +972,7 @@ class VLANTest(APITestCase): response = self.client.delete(url, **self.header) # can't use assertHttpStatus here because we don't have response.data - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 409) content = json.loads(response.content.decode('utf-8')) self.assertIn('detail', content) diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index fbebd09ff..9960fbe42 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -4,7 +4,7 @@ import pytz from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist -from django.db.models import ManyToManyField +from django.db.models import ManyToManyField, ProtectedError from django.http import Http404 from rest_framework.exceptions import APIException from rest_framework.permissions import BasePermission @@ -248,6 +248,22 @@ class ModelViewSet(_ModelViewSet): # Fall back to the hard-coded serializer class return self.serializer_class + def dispatch(self, request, *args, **kwargs): + try: + return super().dispatch(request, *args, **kwargs) + except ProtectedError as e: + models = '\n'.join( + '- {} ({})'.format(o, o._meta) + for o in e.protected_objects.all() + ) + msg = 'You tried deleting a model that is protected by:\n{}'.format(models) + return self.finalize_response( + request, + Response({'detail': msg}, status=409), + *args, + **kwargs + ) + class FieldChoicesViewSet(ViewSet): """ diff --git a/netbox/utilities/middleware.py b/netbox/utilities/middleware.py index 6be01127c..b2065543a 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -1,7 +1,6 @@ from django.conf import settings from django.db import ProgrammingError -from django.http import Http404, HttpResponseRedirect, JsonResponse -from django.db.models import ProtectedError +from django.http import Http404, HttpResponseRedirect from django.urls import reverse from .views import server_error @@ -63,11 +62,6 @@ class ExceptionHandlingMiddleware(object): if isinstance(exception, Http404): return - elif isinstance(exception, ProtectedError): - models = '\n'.join('- {} ({})'.format(o, o._meta) for o in exception.protected_objects.all()) - msg = 'You tried deleting a model that is protected by:\n{}'.format(models) - return JsonResponse({'detail': msg}, status=403) - # Determine the type of exception. If it's a common issue, return a custom error page with instructions. custom_template = None if isinstance(exception, ProgrammingError): From 1ff7e1149ceb21d073b16c457a9eb90ab0511195 Mon Sep 17 00:00:00 2001 From: TakeMeNL Date: Fri, 17 May 2019 23:24:58 +0200 Subject: [PATCH 31/43] Closes #3156: Add site link to rack reservations overview --- CHANGELOG.md | 1 + netbox/dcim/tables.py | 7 ++++++- netbox/dcim/views.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 124b60a95..89dc51292 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * [#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 +* [#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 * [#3186](https://github.com/digitalocean/netbox/issues/3186) - Add interface name filter for IP addresses diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 0266eb010..20c1154f5 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -305,6 +305,11 @@ class RackDetailTable(RackTable): class RackReservationTable(BaseTable): pk = ToggleColumn() + 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') @@ -314,7 +319,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') # diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 85d37b29f..964f1473c 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -458,7 +458,7 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # class RackReservationListView(ObjectListView): - queryset = RackReservation.objects.all() + queryset = RackReservation.objects.select_related('rack__site') filter = filters.RackReservationFilter filter_form = forms.RackReservationFilterForm table = tables.RackReservationTable From 8b005131751833f2f6dd02a70d63db426cf7994f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 May 2019 10:10:07 -0400 Subject: [PATCH 32/43] Changelog for #3031 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72aad08d9..9d17163b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ## 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 From 28facca29189d3af9336b2dc24e66312e8334260 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 May 2019 10:33:29 -0400 Subject: [PATCH 33/43] Changelog & grammar tweak for #3211 --- CHANGELOG.md | 1 + netbox/utilities/api.py | 7 ++----- netbox/utilities/middleware.py | 1 + 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d6fbbd68..1fc7dbebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * [#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" --- diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 9960fbe42..74108fbc9 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -252,11 +252,8 @@ class ModelViewSet(_ModelViewSet): try: return super().dispatch(request, *args, **kwargs) except ProtectedError as e: - models = '\n'.join( - '- {} ({})'.format(o, o._meta) - for o in e.protected_objects.all() - ) - msg = 'You tried deleting a model that is protected by:\n{}'.format(models) + models = ['{} ({})'.format(o, o._meta) for o in e.protected_objects.all()] + msg = 'Unable to delete object. The following dependent objects were found: {}'.format(', '.join(models)) return self.finalize_response( request, Response({'detail': msg}, status=409), diff --git a/netbox/utilities/middleware.py b/netbox/utilities/middleware.py index b2065543a..4e321ab19 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -70,6 +70,7 @@ class ExceptionHandlingMiddleware(object): custom_template = 'exceptions/import_error.html' elif isinstance(exception, PermissionError): custom_template = 'exceptions/permission_error.html' + # Return a custom error message, or fall back to Django's default 500 error handling if custom_template: return server_error(request, template_name=custom_template) From 0804c1acbdacb5e2154cb9093fc9936a34f91e62 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 May 2019 10:51:49 -0400 Subject: [PATCH 34/43] Fixed test from #3211 follow-up work --- netbox/ipam/tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 0dad73550..d43a5675a 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -976,7 +976,7 @@ class VLANTest(APITestCase): content = json.loads(response.content.decode('utf-8')) self.assertIn('detail', content) - self.assertTrue(content['detail'].startswith('You tried deleting a model that is protected by:')) + self.assertTrue(content['detail'].startswith('Unable to delete object.')) class ServiceTest(APITestCase): From 823257ca7277346b26c1dac909393624c7b4c6bf Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 May 2019 15:04:57 -0400 Subject: [PATCH 35/43] Closes #3185: Improve performance for custom field access within templates --- CHANGELOG.md | 1 + netbox/extras/models.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fc7dbebd..f22f4031c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * [#3138](https://github.com/digitalocean/netbox/issues/3138) - Add 2.5GE and 5GE interface form factors * [#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 diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 4c1f9cb76..8d8a05e10 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -102,6 +102,7 @@ class Webhook(models.Model): # class CustomFieldModel(models.Model): + _cf = None class Meta: abstract = True @@ -111,9 +112,12 @@ class CustomFieldModel(models.Model): """ 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): """ @@ -126,7 +130,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: From a6ff6505c60695ae0398dd96a282c32f58b3402b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 May 2019 15:20:36 -0400 Subject: [PATCH 36/43] Closes #3151: Add inventory item count to manufacturers list --- CHANGELOG.md | 1 + netbox/dcim/tables.py | 24 +++++++++++++++++------- netbox/dcim/views.py | 1 + 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f22f4031c..ba433b3d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * [#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 diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 20c1154f5..aec96d04f 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -328,16 +328,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/views.py b/netbox/dcim/views.py index 964f1473c..3cbb2a508 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -516,6 +516,7 @@ class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ManufacturerListView(ObjectListView): 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 From b9b009c0b5668179feedf719ac704f81c5bcb68c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 May 2019 17:17:06 -0400 Subject: [PATCH 37/43] Fixes #3227: Fix exception when deleting a circuit with a termination(s) --- CHANGELOG.md | 1 + netbox/circuits/models.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba433b3d8..b70c1dde6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * [#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) --- diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index b558d5007..cd9cc694a 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() From 1e1aba73ef322aef7cfc4ee0695501c5a9bbde5d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 May 2019 10:32:09 -0400 Subject: [PATCH 38/43] Remove request.user assertion from ObjectChangeMiddleware --- netbox/extras/middleware.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/netbox/extras/middleware.py b/netbox/extras/middleware.py index be878918b..9c8e7b69d 100644 --- a/netbox/extras/middleware.py +++ b/netbox/extras/middleware.py @@ -29,10 +29,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) @@ -47,7 +43,7 @@ class ObjectChangeMiddleware(object): 1. Create an ObjectChange to reflect the modification to the object in the changelog. 2. Enqueue any relevant webhooks. - 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 @@ -65,10 +61,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') From 7d053f8ba4e723b2de503d2a24bec89c6dd192d1 Mon Sep 17 00:00:00 2001 From: dansheps Date: Thu, 30 May 2019 10:54:29 -0500 Subject: [PATCH 39/43] Fix #3228 - Send full path info instead of just path info and urlencode said path info --- CHANGELOG.md | 1 + netbox/utilities/middleware.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b70c1dde6..d97ea98f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ## Bug Fixes +* [#3228](https://github.com/digitalocean/netbox/issues/3228) - Fixed login link retaining query parameters * [#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 diff --git a/netbox/utilities/middleware.py b/netbox/utilities/middleware.py index 4e321ab19..0721d0b8c 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -2,6 +2,7 @@ from django.conf import settings from django.db import ProgrammingError from django.http import Http404, HttpResponseRedirect from django.urls import reverse +import urllib from .views import server_error @@ -22,7 +23,7 @@ class LoginRequiredMiddleware(object): # performs its own authentication. api_path = reverse('api-root') if not request.path_info.startswith(api_path) and request.path_info != settings.LOGIN_URL: - return HttpResponseRedirect('{}?next={}'.format(settings.LOGIN_URL, request.path_info)) + return HttpResponseRedirect('{}?next={}'.format(settings.LOGIN_URL, urllib.parse.quote(request.get_full_path_info())) return self.get_response(request) From a11b33d2144730f451f8fbbebd35c89437a89524 Mon Sep 17 00:00:00 2001 From: dansheps Date: Thu, 30 May 2019 10:54:29 -0500 Subject: [PATCH 40/43] Fix #3228 - Send full path info instead of just path info and urlencode said path info --- CHANGELOG.md | 1 + netbox/utilities/middleware.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b70c1dde6..d97ea98f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ## Bug Fixes +* [#3228](https://github.com/digitalocean/netbox/issues/3228) - Fixed login link retaining query parameters * [#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 diff --git a/netbox/utilities/middleware.py b/netbox/utilities/middleware.py index 4e321ab19..c70be72ed 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -2,6 +2,7 @@ from django.conf import settings from django.db import ProgrammingError from django.http import Http404, HttpResponseRedirect from django.urls import reverse +import urllib from .views import server_error @@ -22,7 +23,8 @@ class LoginRequiredMiddleware(object): # performs its own authentication. api_path = reverse('api-root') if not request.path_info.startswith(api_path) and request.path_info != settings.LOGIN_URL: - return HttpResponseRedirect('{}?next={}'.format(settings.LOGIN_URL, request.path_info)) + return HttpResponseRedirect('{}?next={}'.format(settings.LOGIN_URL, + urllib.parse.quote(request.get_full_path_info()))) return self.get_response(request) From 814c50f46101deb77e9bf2b71c4b1663a6ed953b Mon Sep 17 00:00:00 2001 From: dansheps Date: Thu, 30 May 2019 12:01:41 -0500 Subject: [PATCH 41/43] Fix #3228 - UrlEncode full path for next if not on logon page Include the full path for the ?next= variable in login links if we are not on the logon page. Additionally include next for post requests that have the next variable set (will only come from the login page itself generally) --- netbox/templates/inc/nav_menu.html | 7 ++++++- netbox/templates/login.html | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index 5f8f371d3..177e5df58 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -413,7 +413,12 @@ {% else %} -
  • Log in
  • + {% url 'login' as login_url %} + {% if request.path == login_url %} +
  • Log in
  • + {% else %} +
  • Log in
  • + {% endif %} {% endif %}