From 71087415e376310eaf2ba10036991ae111e24761 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 22dabb0e1cc3264ec9fc41bd3473d472c34ce1e6 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 35c849c60c2446fa763f5bdaaa9c3b5dfdbf3f28 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 60a065de88fbc44bc82db637f48e26a20a609b7f 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 2e0612976c19ad8c3092074b7bcd8ed37d411771 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 e0f27656b50363d68a33d7147ca878887b42785d 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 94f0d3468fbb9ff13f46a8731f287129878e780b 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 0ca77b860592950960ee4441d1865cf007b322e7 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 f7f7d3a567113ac7a6216f3453ba12c932f3bc76 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 3b64cc1dffa8149fd58b28720f69bd735cac2c64 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 6a796a33031ccfdc69ff21025b48c967111ef40c 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 cecb2dd63f9bcb408b24975603f0b1248d6445dd 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 805a96de6f5f386f8a5fe977bb16ae3eeb4ef7dd 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 42ffb121e8fe53efad8c62f4c38f5bce7a55b35a 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 9c597d7a965fdd723859bb855fef204aae04bf2a 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 592ab864e89cd3756ccdbed7e7815a3909d5303c 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 f395820b76f01c80ca945d52bcd76a1f4b6ba160 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 53257a5838bb9e09026a5bc2dce2ebe4272725cb 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 29aab8611f84ed7352dd2d4d822726998c066f72 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 591627e0485c3512c4bafb50be05a8b0c1c6fc32 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 5a57c9b6b791809ea96da9bbae445943e609734c 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 09696c18ae6b40d3035befbc62d07282b1dc1627 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 d264c5f01129585668b6e55a1932da1e26f21108 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 228aef03454164f8b79561d69e49c5554b321818 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 9e0f708432794ce31bca0a7d88404ad974be8fcc 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 98e7c38457c0d25a6fd42c77f6e4ed23c26035a7 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 8b8190f9b4d7108cb168927287e3f88bbfd703e6 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 4f0e0b564221d6a65ab5f43191575ddef6395078 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 1c05951957b1674bd6d386a2a00f294ff3fed35a 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 3192319f2abfbc7cdc198816a7b049ee1bcb9a8a 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 c07652d5d16bf2577bbecd3173af7f80663030f9 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 938ceb5ac675d1ac1a12bb83103e8b65d2c088a7 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 5aa4772507cad9e6dc3626ddc84faa42e86f0afa 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 f4560467a3286f8e042343e039054c4fb92fe8b6 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 1c807fa212afd33424bd058ef4752cfa2dc52951 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 7aabb6fa32d86d9ba2cf34ea3758fe3c459926dc 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 fec3ee9ed3d4e81fba5da4826d7aa59060700c3d 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 e5094ca6b4e195afca9a8c39a678ec952cc0138a 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 29b7fb34719e8997f714bb2f011a51d471098926 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 409b8e8e6afa7b070208f890f0f68e02963ff169 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 daa6f454b23f62d55bd44b48bbf3d49fd66b559f 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 %}