From f78c228c757832abd7fabe41fa90ecd1d9eda3ea Mon Sep 17 00:00:00 2001 From: dansheps Date: Sat, 23 Feb 2019 10:37:30 -0600 Subject: [PATCH 01/43] Fixes #2813: Add Filter for TenantGroup to the following Forms and Filter classes: * circuit.Circuit * dcim.Site * dcim.Rack * dcim.RackElevation * dcim.RackReservation * dcim.Device * ipam.IPAddress * ipam.Prefix * ipam.VRF * ipam.VLAN * virtualization.VirtualMachine --- netbox/circuits/filters.py | 14 ++++++++- netbox/circuits/forms.py | 15 ++++++++- netbox/dcim/filters.py | 50 ++++++++++++++++++++++++++++- netbox/dcim/forms.py | 54 +++++++++++++++++++++++++++++++- netbox/ipam/filters.py | 50 ++++++++++++++++++++++++++++- netbox/ipam/forms.py | 54 +++++++++++++++++++++++++++++++- netbox/virtualization/filters.py | 14 ++++++++- netbox/virtualization/forms.py | 15 ++++++++- 8 files changed, 258 insertions(+), 8 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 12955eeca45..f970c828e1d 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 4deee57c953..2a508b3c274 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 96ecefafdfa..4710800c5f4 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 ad209c5164a..9c7060bd73c 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 f7125ceb018..9e6d4006e34 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 d0e25f580c4..8bc20752796 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 0b7e57ba7ba..32af27adca0 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 70bbf091047..931dccc5da9 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -8,7 +8,7 @@ from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, S from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm from ipam.models import IPAddress from tenancy.forms import TenancyForm -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm, @@ -591,6 +591,19 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, widget=StaticSelect2Multiple() ) + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + null_option=True, + filter_for={ + 'tenant': 'group' + } + ) + ) tenant = FilterChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', From 8683efe54ae438b589fcd9fdb580956d4024abef Mon Sep 17 00:00:00 2001 From: dansheps Date: Sat, 23 Feb 2019 11:09:02 -0600 Subject: [PATCH 02/43] Fixes #2813: Add Filter and List View for TenantGroup Added Filter for TenantGroup to the following Forms and Filter classes * circuit.Circuit * dcim.Site * dcim.Rack * dcim.RackElevation * dcim.RackReservation * dcim.Device * ipam.IPAddress * ipam.Prefix * ipam.VRF * ipam.VLAN * virtualization.VirtualMachine Added List View to the following classes: * circuit.Circuit * dcim.Site * dcim.Rack * dcim.RackReservation * dcim.Device * ipam.IPAddress * ipam.Prefix * ipam.VRF * ipam.VLAN * virtualization.VirtualMachine --- netbox/circuits/tables.py | 4 ++-- netbox/dcim/tables.py | 10 +++++----- netbox/ipam/tables.py | 15 +++++++++++---- netbox/tenancy/tables.py | 10 ++++++++++ netbox/virtualization/tables.py | 4 ++-- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index c6a215db883..f90a761a70f 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 5649c10ef24..11dcca81a85 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 026cbc980b2..2ae9d562a79 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 91122df7a67..779d3bd2ec6 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 b825ba59f37..354f9025d2b 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Interface -from tenancy.tables import COL_TENANT +from tenancy.tables import COL_TENANTGROUP_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -103,7 +103,7 @@ class VirtualMachineTable(BaseTable): status = tables.TemplateColumn(template_code=VIRTUALMACHINE_STATUS) cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')]) role = tables.TemplateColumn(VIRTUALMACHINE_ROLE) - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) class Meta(BaseTable.Meta): model = VirtualMachine From 679aa0f764d6b0c830d093cd59c9a753757c49e0 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 26 Feb 2019 07:53:59 -0600 Subject: [PATCH 03/43] Update tables.py Fix whitespace --- netbox/ipam/tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 2ae9d562a79..ff8cf8928af 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Interface -from tenancy.tables import COL_TENANT,COL_TENANTGROUP_TENANT +from tenancy.tables import COL_TENANT, COL_TENANTGROUP_TENANT from utilities.tables import BaseTable, BooleanColumn, ToggleColumn from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF From b4d7f9ea43564567b19a8dbe203d1b21d3f64aff Mon Sep 17 00:00:00 2001 From: dansheps Date: Tue, 5 Mar 2019 08:10:10 -0600 Subject: [PATCH 04/43] Fixes #2781: Fixes filter by regions on site and device list * Add Device filter --- netbox/circuits/filters.py | 26 +------ netbox/circuits/forms.py | 31 ++------- netbox/circuits/tables.py | 4 +- netbox/dcim/filters.py | 111 ++---------------------------- netbox/dcim/forms.py | 112 ++++--------------------------- netbox/dcim/tables.py | 10 +-- netbox/ipam/filters.py | 98 ++------------------------- netbox/ipam/forms.py | 112 ++++--------------------------- netbox/ipam/tables.py | 12 ++-- netbox/tenancy/filters.py | 25 +++++++ netbox/tenancy/forms.py | 28 ++++++++ netbox/virtualization/filters.py | 26 +------ netbox/virtualization/forms.py | 31 ++------- netbox/virtualization/tables.py | 4 +- 14 files changed, 121 insertions(+), 509 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index f970c828e1d..f54567f6897 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 2a508b3c274..95237ab1c82 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 f90a761a70f..c6a215db883 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 4710800c5f4..7d609426d1c 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 9c7060bd73c..f851e0938b5 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 11dcca81a85..b61bfed6427 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 9e6d4006e34..da60c9f3d0a 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 8bc20752796..6b97c1f8332 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 2ae9d562a79..7a5ad97c354 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 745391898b1..a7bf19f5960 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 3c97eb801e6..6e2c59cd483 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 32af27adca0..5224d11bac2 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 931dccc5da9..eb194e6b264 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 354f9025d2b..b825ba59f37 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Interface -from tenancy.tables import COL_TENANTGROUP_TENANT +from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -103,7 +103,7 @@ class VirtualMachineTable(BaseTable): status = tables.TemplateColumn(template_code=VIRTUALMACHINE_STATUS) cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')]) role = tables.TemplateColumn(VIRTUALMACHINE_ROLE) - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(BaseTable.Meta): model = VirtualMachine From 37811d3f7e3c04d7d2828f7695975e133fd01cbd Mon Sep 17 00:00:00 2001 From: dansheps Date: Tue, 5 Mar 2019 08:19:21 -0600 Subject: [PATCH 05/43] * Resolve conflict with virtualization filters. --- netbox/virtualization/filters.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 0e5ff6cd273..06b8f8553cb 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -6,7 +6,7 @@ from netaddr.core import AddrFormatError from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant +from tenancy.filters import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -80,7 +80,7 @@ class ClusterFilter(CustomFieldFilterSet): ) -class VirtualMachineFilter(CustomFieldFilterSet): +class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -151,16 +151,6 @@ class VirtualMachineFilter(CustomFieldFilterSet): to_field_name='slug', label='Role (slug)', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) platform_id = django_filters.ModelMultipleChoiceFilter( queryset=Platform.objects.all(), label='Platform (ID)', From 6e8e6809f344e335bd07834ae4cee35d4690c7a3 Mon Sep 17 00:00:00 2001 From: dansheps Date: Wed, 10 Apr 2019 08:37:12 -0500 Subject: [PATCH 06/43] Move Filter and Form to new file, update all files --- netbox/circuits/filters.py | 2 +- netbox/circuits/forms.py | 3 ++- netbox/dcim/filters.py | 2 +- netbox/dcim/forms.py | 3 ++- netbox/extras/filters.py | 26 ++------------------------ netbox/extras/forms.py | 24 ++++++------------------ netbox/ipam/filters.py | 2 +- netbox/ipam/forms.py | 3 ++- netbox/tenancy/filters.py | 25 ------------------------- netbox/tenancy/filterset.py | 27 +++++++++++++++++++++++++++ netbox/tenancy/forms.py | 32 +------------------------------- netbox/tenancy/formset.py | 31 +++++++++++++++++++++++++++++++ netbox/virtualization/filters.py | 2 +- netbox/virtualization/forms.py | 3 ++- 14 files changed, 79 insertions(+), 106 deletions(-) create mode 100644 netbox/tenancy/filterset.py create mode 100644 netbox/tenancy/formset.py diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index f54567f6897..e79b78e7b63 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 95237ab1c82..f90cb950366 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 dc2e411d6a7..2695cc7a525 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 bf40f172a81..699e2635c9b 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 d5457a5a6fd..712dfd6d8b7 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 54eee0c5c45..a956597d3e8 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 da60c9f3d0a..3050a790133 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 f731db3539d..1bc83a9d06c 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 b2c8e7934f4..2610b3ec0c1 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 00000000000..8980777b09e --- /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 6e2c59cd483..6095b1f5b07 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 00000000000..feac6a9cc24 --- /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 06b8f8553cb..a3ae66f3d16 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 911c7064a7b..2e881b18408 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -7,7 +7,8 @@ from dcim.forms import INTERFACE_MODE_HELP_TEXT from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm from ipam.models import IPAddress -from tenancy.forms import TenancyForm, TenancyFilterForm +from tenancy.forms import TenancyForm +from tenancy.formset import TenancyFilterForm from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, From 6fa54bed73aaa27ed9307c6ddac3b8def4b36666 Mon Sep 17 00:00:00 2001 From: dansheps Date: Wed, 10 Apr 2019 08:42:27 -0500 Subject: [PATCH 07/43] Fix PEP8 Errors --- netbox/tenancy/forms.py | 2 +- netbox/tenancy/formset.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 6095b1f5b07..e7e065ff7c9 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 feac6a9cc24..e07517bbe3c 100644 --- a/netbox/tenancy/formset.py +++ b/netbox/tenancy/formset.py @@ -2,6 +2,7 @@ from django import forms from utilities.forms import APISelectMultiple, FilterChoiceField from .models import Tenant, TenantGroup + # # Tenancy filtering form extension # @@ -28,4 +29,4 @@ class TenancyFilterForm(forms.Form): value_field="slug", null_option=True, ) - ) \ No newline at end of file + ) From 22e5834d8b5f42b8408dedcd1cbd9f696f1deb42 Mon Sep 17 00:00:00 2001 From: dansheps Date: Tue, 30 Apr 2019 10:06:27 -0500 Subject: [PATCH 08/43] Remove tenant group from ipam table --- netbox/ipam/tables.py | 6 +----- netbox/tenancy/tables.py | 10 ---------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 8d11d8f77f5..9578b440765 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 4bb8a4f772c..884bdc3df69 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 49446ffb743f957b8170f9aad39b14c15491d4ee Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 1 May 2019 11:09:11 -0400 Subject: [PATCH 09/43] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 92b40cd5b04..1708c07f0ab 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ except ImportError: ) -VERSION = '2.5.12' +VERSION = '2.5.13-dev' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From ee4a3bcb0251b7f3d00dd9e4b001c190e81b0ae2 Mon Sep 17 00:00:00 2001 From: Shane Madden Date: Wed, 1 May 2019 13:47:52 -0600 Subject: [PATCH 10/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 926b97130b2..9bd54954184 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -360,7 +360,7 @@ CONNECTION_STATUS_CHOICES = [ # Cable endpoint types CABLE_TERMINATION_TYPES = [ - 'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', + 'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination', ] # Cable types From 5f5e4ce1a13ad41401ef86e54a8f9786852865d1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 May 2019 11:41:37 -0400 Subject: [PATCH 11/43] Changelog for #3132 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82a4a7e41ae..1b3d827cdfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +2.5.13 (FUTURE) + +## Bug Fixes + +* [#3132](https://github.com/digitalocean/netbox/issues/3132) - Circuit termination missing from available cable termination types + +--- + 2.5.12 (2019-05-01) ## Bug Fixes From 01c5d9e9095339c386faf033efaffad480b10aa0 Mon Sep 17 00:00:00 2001 From: Austin English Date: Thu, 2 May 2019 13:02:53 -0500 Subject: [PATCH 12/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 24e79f5bdb9..880d27d5dfe 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -5,6 +5,8 @@ # Once the script completes, remember to restart the WSGI service (e.g. # gunicorn or uWSGI). +cd "$(dirname "$0")" + PYTHON="python3" PIP="pip3" From 244c07e5f7fcc2ef16c839a55ba232437040463f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 May 2019 15:36:30 -0400 Subject: [PATCH 13/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 1b3d827cdfc..6662a046e6a 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 f52f4ea9eea..8b92e5a59f2 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -126,10 +126,12 @@ class ObjectListView(View): queryset = CustomFieldQueryset(self.queryset, custom_fields) if custom_fields else self.queryset try: return et.render_to_response(queryset) - except TemplateSyntaxError: + except Exception as e: messages.error( request, - "There was an error rendering the selected export template ({}).".format(et.name) + "There was an error rendering the selected export template ({}): {}".format( + et.name, e + ) ) # Fall back to built-in CSV formatting if export requested but no template specified From 1b862045e3f933a867010ed87107c62b1c1e22dd Mon Sep 17 00:00:00 2001 From: Oli <932481+tb-killa@users.noreply.github.com> Date: Mon, 6 May 2019 15:36:44 +0200 Subject: [PATCH 14/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 7ef88543eab..08ecf783d25 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -31,7 +31,7 @@

{{ cable.get_status_display }}

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

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

No Cable

From b97339017bb3cc8ed2bfb404f7c4566bdacfbe8f Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Mon, 6 May 2019 11:54:16 -0500 Subject: [PATCH 15/43] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6662a046e6a..262c38c1abd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Enhancements +* [#3150](https://github.com/digitalocean/netbox/issues/3150) - Fix formatting of cable length during cable trace * [#3085](https://github.com/digitalocean/netbox/issues/3085) - Catch all exceptions during export template rendering ## Bug Fixes From fbde6282b218d956842ce509e7e5ae5d64181ac9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 9 May 2019 14:32:49 -0400 Subject: [PATCH 16/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 262c38c1abd..a5ab7badb3a 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 e79b78e7b63..4decb716616 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 f90cb950366..8e5fbf9f7f1 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 47f052f709a..f9f0a57d68f 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 56a1cb9b4b3..369ebeb2381 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 712dfd6d8b7..d5457a5a6fd 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 a956597d3e8..54eee0c5c45 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 3050a790133..a6e9861170e 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 1bc83a9d06c..a8e9df6c575 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 e7e065ff7c9..636cfed0316 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 a3ae66f3d16..e71693ac633 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 2e881b18408..e61c0e32033 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -9,7 +9,7 @@ from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomField from ipam.models import IPAddress from tenancy.forms import TenancyForm from tenancy.formset import TenancyFilterForm -from tenancy.models import Tenant, TenantGroup +from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm, @@ -337,8 +337,8 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): class Meta: model = VirtualMachine fields = [ - 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', - 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data', + 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', + 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data', ] help_texts = { 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered " @@ -523,8 +523,10 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VirtualMachine - # Order the form fields, fields not listed are appended - field_order = ['q', 'cluster_group', 'cluster_type', 'cluster_id', 'region', 'site'] + field_order = [ + 'q', 'cluster_group', 'cluster_type', 'cluster_id', 'status', 'role', 'region', 'site', 'tenant_group', + 'tenant', 'platform', + ] q = forms.CharField( required=False, label='Search' From e19feb92eaa9a0f297e15d79b41597eeb2b6a415 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 9 May 2019 14:36:18 -0400 Subject: [PATCH 17/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 8e5fbf9f7f1..100c6334f29 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 369ebeb2381..f10418d57b5 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 a8e9df6c575..d7c0b1a0c58 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 636cfed0316..f8aaa45e58d 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 e07517bbe3c..00000000000 --- 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 e61c0e32033..59b340e8583 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -8,7 +8,7 @@ from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, S from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm from ipam.models import IPAddress from tenancy.forms import TenancyForm -from tenancy.formset import TenancyFilterForm +from tenancy.forms import TenancyFilterForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, From 62d497dd0bf7f829c9c1dc6b28e63b6455e2a871 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 14 May 2019 19:03:03 -0400 Subject: [PATCH 18/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 a5ab7badb3a..6014da3dc91 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 a6e9861170e..a5464e4d0d5 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -286,6 +286,12 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet): to_field_name='name', label='Virtual machine (name)', ) + interface = django_filters.ModelMultipleChoiceFilter( + field_name='interface__name', + queryset=Interface.objects.all(), + to_field_name='name', + label='Interface (ID)', + ) interface_id = django_filters.ModelMultipleChoiceFilter( queryset=Interface.objects.all(), label='Interface (ID)', From 2f32488c25610d3e6c4db2c8731fac58b655859e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 May 2019 19:45:36 -0400 Subject: [PATCH 19/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 6014da3dc91..a63cd9f87fb 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 da8f09a50bc..4c1f9cb7629 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 439323c943a..70c93968ff0 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 8b92e5a59f2..fd54088ba86 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -1,5 +1,4 @@ import sys -from collections import OrderedDict from copy import deepcopy from django.conf import settings @@ -12,7 +11,7 @@ from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHidd from django.http import HttpResponse, HttpResponseServerError from django.shortcuts import get_object_or_404, redirect, render from django.template import loader -from django.template.exceptions import TemplateDoesNotExist, TemplateSyntaxError +from django.template.exceptions import TemplateDoesNotExist from django.urls import reverse from django.utils.html import escape from django.utils.http import is_safe_url @@ -23,6 +22,7 @@ from django.views.generic import View from django_tables2 import RequestConfig from extras.models import CustomField, CustomFieldValue, ExportTemplate +from extras.querysets import CustomFieldQueryset from utilities.forms import BootstrapMixin, CSVDataField from utilities.utils import csv_format from .error_handlers import handle_protectederror @@ -30,23 +30,6 @@ from .forms import ConfirmationForm from .paginator import EnhancedPaginator -class CustomFieldQueryset: - """ - Annotate custom fields on objects within a QuerySet. - """ - - def __init__(self, queryset, custom_fields): - self.queryset = queryset - self.model = queryset.model - self.custom_fields = custom_fields - - def __iter__(self): - for obj in self.queryset: - values_dict = {cfv.field_id: cfv.value for cfv in obj.custom_field_values.all()} - obj.custom_fields = OrderedDict([(field, values_dict.get(field.pk)) for field in self.custom_fields]) - yield obj - - class GetReturnURLMixin(object): """ Provides logic for determining where a user should be redirected after processing a form. @@ -115,8 +98,9 @@ class ObjectListView(View): self.queryset = self.filter(request.GET, self.queryset).qs # If this type of object has one or more custom fields, prefetch any relevant custom field values - custom_fields = CustomField.objects.filter(obj_type=ContentType.objects.get_for_model(model))\ - .prefetch_related('choices') + custom_fields = CustomField.objects.filter( + obj_type=ContentType.objects.get_for_model(model) + ).prefetch_related('choices') if custom_fields: self.queryset = self.queryset.prefetch_related('custom_field_values') From 9b47e57e8e6af58ab324772d04c162f1a9daf020 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 May 2019 20:24:55 -0400 Subject: [PATCH 20/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 a63cd9f87fb..44c6bc45d99 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 21d620af15e..07cf4b0108f 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 27f90a3a204..85d37b29f55 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 0dd52a96d27..64948a6f96c 100644 --- a/netbox/templates/dcim/site_list.html +++ b/netbox/templates/dcim/site_list.html @@ -12,7 +12,7 @@

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

- {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' %} + {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' bulk_delete_url='dcim:site_bulk_delete' %}
{% include 'inc/search_panel.html' %} From edabc8eee96313427605de63ab30e5f62e46975d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 May 2019 20:49:00 -0400 Subject: [PATCH 21/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 44c6bc45d99..124b60a95cc 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 9bd54954184..ea7bded2b3e 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -75,6 +75,8 @@ IFACE_FF_100ME_FIXED = 800 IFACE_FF_1GE_FIXED = 1000 IFACE_FF_1GE_GBIC = 1050 IFACE_FF_1GE_SFP = 1100 +IFACE_FF_2GE_FIXED = 1120 +IFACE_FF_5GE_FIXED = 1130 IFACE_FF_10GE_FIXED = 1150 IFACE_FF_10GE_CX4 = 1170 IFACE_FF_10GE_SFP_PLUS = 1200 @@ -150,6 +152,8 @@ IFACE_FF_CHOICES = [ [ [IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'], [IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'], + [IFACE_FF_2GE_FIXED, '2.5GBASE-T (2.5GE)'], + [IFACE_FF_5GE_FIXED, '5GBASE-T (5GE)'], [IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'], [IFACE_FF_10GE_CX4, '10GBASE-CX4 (10GE)'], ] From cbace6f83178fe49f6d52bda428a4868fcd749ad Mon Sep 17 00:00:00 2001 From: Khaled BEN ABDALLAH Date: Sat, 18 May 2019 21:06:22 +0200 Subject: [PATCH 22/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 96d59ace5a1..e8cc6aa1f43 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -267,6 +267,10 @@ $(document).ready(function() { processResults: function (data) { var results = $.map(data.results, function (obj) { + // If tag contains space add double quotes + if (/\s/.test(obj.name)) + obj.name = '"' + obj.name + '"' + return { id: obj.name, text: obj.name From 4313a717c4111c0dd34465375b8795e0ef7b7db5 Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Mon, 20 May 2019 21:06:53 +0100 Subject: [PATCH 23/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 26ca5022006..e87811bc136 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -559,6 +559,7 @@ table.report th a { .color-block { display: block; width: 80px; + border: 1px solid grey; } .text-nowrap { white-space: nowrap; From f9cd89a4a454e91f721e5c3f49cda29ebc41e578 Mon Sep 17 00:00:00 2001 From: hellerve Date: Sun, 26 May 2019 14:38:27 +0200 Subject: [PATCH 24/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 be110630892..440960d646c 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 07cf4b0108f..25c3a5a4d00 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 35a6fb11029..5ba7c110ce5 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 c2f7badd3b7..2a1dcdf05da 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 45c99beb9c2..ad3806c6f6d 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 e1ce2b8f223..9d07dd63c71 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 19522e6c757..fb23a6ef166 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 a45f859e71f..40fdbeab1f6 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 5fc5997a853..7cc28be51b3 100644 --- a/netbox/virtualization/urls.py +++ b/netbox/virtualization/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from extras.views import ObjectChangeLogView from ipam.views import ServiceCreateView @@ -9,53 +9,53 @@ app_name = 'virtualization' urlpatterns = [ # Cluster types - url(r'^cluster-types/$', views.ClusterTypeListView.as_view(), name='clustertype_list'), - url(r'^cluster-types/add/$', views.ClusterTypeCreateView.as_view(), name='clustertype_add'), - url(r'^cluster-types/import/$', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'), - url(r'^cluster-types/delete/$', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'), - url(r'^cluster-types/(?P[\w-]+)/edit/$', views.ClusterTypeEditView.as_view(), name='clustertype_edit'), - url(r'^cluster-types/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}), + path(r'cluster-types/', views.ClusterTypeListView.as_view(), name='clustertype_list'), + path(r'cluster-types/add/', views.ClusterTypeCreateView.as_view(), name='clustertype_add'), + path(r'cluster-types/import/', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'), + path(r'cluster-types/delete/', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'), + path(r'cluster-types//edit/', views.ClusterTypeEditView.as_view(), name='clustertype_edit'), + path(r'cluster-types//changelog/', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}), # Cluster groups - url(r'^cluster-groups/$', views.ClusterGroupListView.as_view(), name='clustergroup_list'), - url(r'^cluster-groups/add/$', views.ClusterGroupCreateView.as_view(), name='clustergroup_add'), - url(r'^cluster-groups/import/$', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'), - url(r'^cluster-groups/delete/$', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'), - url(r'^cluster-groups/(?P[\w-]+)/edit/$', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'), - url(r'^cluster-groups/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}), + path(r'cluster-groups/', views.ClusterGroupListView.as_view(), name='clustergroup_list'), + path(r'cluster-groups/add/', views.ClusterGroupCreateView.as_view(), name='clustergroup_add'), + path(r'cluster-groups/import/', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'), + path(r'cluster-groups/delete/', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'), + path(r'cluster-groups//edit/', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'), + path(r'cluster-groups//changelog/', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}), # Clusters - url(r'^clusters/$', views.ClusterListView.as_view(), name='cluster_list'), - url(r'^clusters/add/$', views.ClusterCreateView.as_view(), name='cluster_add'), - url(r'^clusters/import/$', views.ClusterBulkImportView.as_view(), name='cluster_import'), - url(r'^clusters/edit/$', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'), - url(r'^clusters/delete/$', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'), - url(r'^clusters/(?P\d+)/$', views.ClusterView.as_view(), name='cluster'), - url(r'^clusters/(?P\d+)/edit/$', views.ClusterEditView.as_view(), name='cluster_edit'), - url(r'^clusters/(?P\d+)/delete/$', views.ClusterDeleteView.as_view(), name='cluster_delete'), - url(r'^clusters/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}), - url(r'^clusters/(?P\d+)/devices/add/$', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'), - url(r'^clusters/(?P\d+)/devices/remove/$', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'), + path(r'clusters/', views.ClusterListView.as_view(), name='cluster_list'), + path(r'clusters/add/', views.ClusterCreateView.as_view(), name='cluster_add'), + path(r'clusters/import/', views.ClusterBulkImportView.as_view(), name='cluster_import'), + path(r'clusters/edit/', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'), + path(r'clusters/delete/', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'), + path(r'clusters//', views.ClusterView.as_view(), name='cluster'), + path(r'clusters//edit/', views.ClusterEditView.as_view(), name='cluster_edit'), + path(r'clusters//delete/', views.ClusterDeleteView.as_view(), name='cluster_delete'), + path(r'clusters//changelog/', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}), + path(r'clusters//devices/add/', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'), + path(r'clusters//devices/remove/', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'), # Virtual machines - url(r'^virtual-machines/$', views.VirtualMachineListView.as_view(), name='virtualmachine_list'), - url(r'^virtual-machines/add/$', views.VirtualMachineCreateView.as_view(), name='virtualmachine_add'), - url(r'^virtual-machines/import/$', views.VirtualMachineBulkImportView.as_view(), name='virtualmachine_import'), - url(r'^virtual-machines/edit/$', views.VirtualMachineBulkEditView.as_view(), name='virtualmachine_bulk_edit'), - url(r'^virtual-machines/delete/$', views.VirtualMachineBulkDeleteView.as_view(), name='virtualmachine_bulk_delete'), - url(r'^virtual-machines/(?P\d+)/$', views.VirtualMachineView.as_view(), name='virtualmachine'), - url(r'^virtual-machines/(?P\d+)/edit/$', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'), - url(r'^virtual-machines/(?P\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'), - url(r'^virtual-machines/(?P\d+)/config-context/$', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'), - url(r'^virtual-machines/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}), - url(r'^virtual-machines/(?P\d+)/services/assign/$', ServiceCreateView.as_view(), name='virtualmachine_service_assign'), + path(r'virtual-machines/', views.VirtualMachineListView.as_view(), name='virtualmachine_list'), + path(r'virtual-machines/add/', views.VirtualMachineCreateView.as_view(), name='virtualmachine_add'), + path(r'virtual-machines/import/', views.VirtualMachineBulkImportView.as_view(), name='virtualmachine_import'), + path(r'virtual-machines/edit/', views.VirtualMachineBulkEditView.as_view(), name='virtualmachine_bulk_edit'), + path(r'virtual-machines/delete/', views.VirtualMachineBulkDeleteView.as_view(), name='virtualmachine_bulk_delete'), + path(r'virtual-machines//', views.VirtualMachineView.as_view(), name='virtualmachine'), + path(r'virtual-machines//edit/', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'), + path(r'virtual-machines//delete/', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'), + path(r'virtual-machines//config-context/', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'), + path(r'virtual-machines//changelog/', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}), + path(r'virtual-machines//services/assign/', ServiceCreateView.as_view(), name='virtualmachine_service_assign'), # VM interfaces - url(r'^virtual-machines/interfaces/add/$', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_interface'), - url(r'^virtual-machines/(?P\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'), - url(r'^virtual-machines/(?P\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), - url(r'^virtual-machines/(?P\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), - url(r'^vm-interfaces/(?P\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'), - url(r'^vm-interfaces/(?P\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'), + path(r'virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_interface'), + path(r'virtual-machines//interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'), + path(r'virtual-machines//interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), + path(r'virtual-machines//interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), + path(r'vm-interfaces//edit/', views.InterfaceEditView.as_view(), name='interface_edit'), + path(r'vm-interfaces//delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), ] From b114b9d39687d143c915ee21bb5673fb7077f173 Mon Sep 17 00:00:00 2001 From: hellerve Date: Sun, 26 May 2019 14:44:28 +0200 Subject: [PATCH 25/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 ad3806c6f6d..6a1db6dea0d 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 00000000000..d8b8f69c49c --- /dev/null +++ b/netbox/utilities/converters.py @@ -0,0 +1,8 @@ +class JSONOrYAMLConverter: + regex = '.json|.yaml' + + def to_python(self, value): + return value + + def to_url(self, value): + return value From 473dafc2c88171f86c8497c751df55f20e0c6275 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 May 2019 15:03:00 -0400 Subject: [PATCH 26/43] Changelog for #3184 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 124b60a95cc..b6ec83534ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * [#3132](https://github.com/digitalocean/netbox/issues/3132) - Circuit termination missing from available cable termination types * [#3150](https://github.com/digitalocean/netbox/issues/3150) - Fix formatting of cable length during cable trace +* [#3184](https://github.com/digitalocean/netbox/issues/3184) - Correctly display color block for white cables * [#3190](https://github.com/digitalocean/netbox/issues/3190) - Fix custom field rendering for Jinja2 export templates --- From 1366730a3fb43e652b894d47629fbac665db1574 Mon Sep 17 00:00:00 2001 From: hellerve Date: Mon, 27 May 2019 22:41:10 +0200 Subject: [PATCH 27/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 6a1db6dea0d..efcd17a8735 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 d8b8f69c49c..00000000000 --- a/netbox/utilities/converters.py +++ /dev/null @@ -1,8 +0,0 @@ -class JSONOrYAMLConverter: - regex = '.json|.yaml' - - def to_python(self, value): - return value - - def to_url(self, value): - return value From cc87d9901785f252d011f486eb95ecf74898dc1a Mon Sep 17 00:00:00 2001 From: hellerve Date: Tue, 28 May 2019 16:11:49 +0200 Subject: [PATCH 28/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 3f4555b55cd..991f098bb17 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 4e321ab1974..6be01127c8d 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -1,6 +1,7 @@ from django.conf import settings from django.db import ProgrammingError -from django.http import Http404, HttpResponseRedirect +from django.http import Http404, HttpResponseRedirect, JsonResponse +from django.db.models import ProtectedError from django.urls import reverse from .views import server_error @@ -62,6 +63,11 @@ class ExceptionHandlingMiddleware(object): if isinstance(exception, Http404): return + elif isinstance(exception, ProtectedError): + models = '\n'.join('- {} ({})'.format(o, o._meta) for o in exception.protected_objects.all()) + msg = 'You tried deleting a model that is protected by:\n{}'.format(models) + return JsonResponse({'detail': msg}, status=403) + # Determine the type of exception. If it's a common issue, return a custom error page with instructions. custom_template = None if isinstance(exception, ProgrammingError): @@ -70,7 +76,6 @@ class ExceptionHandlingMiddleware(object): custom_template = 'exceptions/import_error.html' elif isinstance(exception, PermissionError): custom_template = 'exceptions/permission_error.html' - # Return a custom error message, or fall back to Django's default 500 error handling if custom_template: return server_error(request, template_name=custom_template) From 87f5dd05f5707f40f5987612297a0ce87c3c8af3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 May 2019 13:10:54 -0400 Subject: [PATCH 29/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 b6ec83534ef..72aad08d901 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 f9f0a57d68f..ec1e03983c1 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -606,7 +606,7 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet): return queryset.exclude(powerports__isnull=value) def _power_outlets(self, queryset, name, value): - return queryset.exclude(poweroutlets_isnull=value) + return queryset.exclude(poweroutlets__isnull=value) def _interfaces(self, queryset, name, value): return queryset.exclude(interfaces__isnull=value) From c4f481d705a5a28722f6d58fe5909649bdfee6ca Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 May 2019 14:21:36 -0400 Subject: [PATCH 30/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 0bc96db8d4f..b69ce9475b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ django-tables2==2.0.3 django-taggit==0.23.0 django-taggit-serializer==0.1.7 django-timezone-field==3.0 -djangorestframework==3.9.0 +djangorestframework==3.9.1 drf-yasg[validation]==1.14.0 graphviz==0.10.1 Jinja2==2.10 From 2c7bad9fff4b4741f69c68586dbb624757221805 Mon Sep 17 00:00:00 2001 From: hellerve Date: Tue, 28 May 2019 21:11:23 +0200 Subject: [PATCH 31/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 991f098bb17..0dad7355067 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 fbebd09ffeb..9960fbe4248 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 6be01127c8d..b2065543a40 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -1,7 +1,6 @@ from django.conf import settings from django.db import ProgrammingError -from django.http import Http404, HttpResponseRedirect, JsonResponse -from django.db.models import ProtectedError +from django.http import Http404, HttpResponseRedirect from django.urls import reverse from .views import server_error @@ -63,11 +62,6 @@ class ExceptionHandlingMiddleware(object): if isinstance(exception, Http404): return - elif isinstance(exception, ProtectedError): - models = '\n'.join('- {} ({})'.format(o, o._meta) for o in exception.protected_objects.all()) - msg = 'You tried deleting a model that is protected by:\n{}'.format(models) - return JsonResponse({'detail': msg}, status=403) - # Determine the type of exception. If it's a common issue, return a custom error page with instructions. custom_template = None if isinstance(exception, ProgrammingError): From 1ff7e1149ceb21d073b16c457a9eb90ab0511195 Mon Sep 17 00:00:00 2001 From: TakeMeNL Date: Fri, 17 May 2019 23:24:58 +0200 Subject: [PATCH 32/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 124b60a95cc..89dc51292d0 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 0266eb01089..20c1154f5f0 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 85d37b29f55..964f1473c7b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -458,7 +458,7 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # class RackReservationListView(ObjectListView): - queryset = RackReservation.objects.all() + queryset = RackReservation.objects.select_related('rack__site') filter = filters.RackReservationFilter filter_form = forms.RackReservationFilterForm table = tables.RackReservationTable From 8b005131751833f2f6dd02a70d63db426cf7994f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 May 2019 10:10:07 -0400 Subject: [PATCH 33/43] Changelog for #3031 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72aad08d901..9d17163b216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ## Bug Fixes +* [#3031](https://github.com/digitalocean/netbox/issues/3031) - Fixed form field population of tags with spaces * [#3132](https://github.com/digitalocean/netbox/issues/3132) - Circuit termination missing from available cable termination types * [#3150](https://github.com/digitalocean/netbox/issues/3150) - Fix formatting of cable length during cable trace * [#3184](https://github.com/digitalocean/netbox/issues/3184) - Correctly display color block for white cables From 28facca29189d3af9336b2dc24e66312e8334260 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 May 2019 10:33:29 -0400 Subject: [PATCH 34/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 4d6fbbd68f1..1fc7dbebdff 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 9960fbe4248..74108fbc97e 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 b2065543a40..4e321ab1974 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -70,6 +70,7 @@ class ExceptionHandlingMiddleware(object): custom_template = 'exceptions/import_error.html' elif isinstance(exception, PermissionError): custom_template = 'exceptions/permission_error.html' + # Return a custom error message, or fall back to Django's default 500 error handling if custom_template: return server_error(request, template_name=custom_template) From 0804c1acbdacb5e2154cb9093fc9936a34f91e62 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 May 2019 10:51:49 -0400 Subject: [PATCH 35/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 0dad7355067..d43a5675aed 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -976,7 +976,7 @@ class VLANTest(APITestCase): content = json.loads(response.content.decode('utf-8')) self.assertIn('detail', content) - self.assertTrue(content['detail'].startswith('You tried deleting a model that is protected by:')) + self.assertTrue(content['detail'].startswith('Unable to delete object.')) class ServiceTest(APITestCase): From 823257ca7277346b26c1dac909393624c7b4c6bf Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 May 2019 15:04:57 -0400 Subject: [PATCH 36/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 1fc7dbebdff..f22f4031cb9 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 4c1f9cb7629..8d8a05e10eb 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -102,6 +102,7 @@ class Webhook(models.Model): # class CustomFieldModel(models.Model): + _cf = None class Meta: abstract = True @@ -111,9 +112,12 @@ class CustomFieldModel(models.Model): """ Name-based CustomFieldValue accessor for use in templates """ - if not hasattr(self, 'get_custom_fields'): - return dict() - return {field.name: value for field, value in self.get_custom_fields().items()} + if self._cf is None: + # Cache all custom field values for this instance + self._cf = { + field.name: value for field, value in self.get_custom_fields().items() + } + return self._cf def get_custom_fields(self): """ @@ -126,7 +130,7 @@ class CustomFieldModel(models.Model): # If the object exists, populate its custom fields with values if hasattr(self, 'pk'): - values = CustomFieldValue.objects.filter(obj_type=content_type, obj_id=self.pk).select_related('field') + values = self.custom_field_values.all() values_dict = {cfv.field_id: cfv.value for cfv in values} return OrderedDict([(field, values_dict.get(field.pk)) for field in fields]) else: From a6ff6505c60695ae0398dd96a282c32f58b3402b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 May 2019 15:20:36 -0400 Subject: [PATCH 37/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 f22f4031cb9..ba433b3d879 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 20c1154f5f0..aec96d04f2f 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 964f1473c7b..3cbb2a50869 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -516,6 +516,7 @@ class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ManufacturerListView(ObjectListView): queryset = Manufacturer.objects.annotate( devicetype_count=Count('device_types', distinct=True), + inventoryitem_count=Count('inventory_items', distinct=True), platform_count=Count('platforms', distinct=True), ) table = tables.ManufacturerTable From b9b009c0b5668179feedf719ac704f81c5bcb68c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 May 2019 17:17:06 -0400 Subject: [PATCH 38/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 ba433b3d879..b70c1dde6a7 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 b558d50073e..cd9cc694ad3 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -274,11 +274,16 @@ class CircuitTermination(CableTermination): """ Reference the parent circuit when recording the change. """ + try: + related_object = self.circuit + except Circuit.DoesNotExist: + # Parent circuit has been deleted + related_object = None ObjectChange( user=user, request_id=request_id, changed_object=self, - related_object=self.circuit, + related_object=related_object, action=action, object_data=serialize_object(self) ).save() From 1e1aba73ef322aef7cfc4ee0695501c5a9bbde5d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 May 2019 10:32:09 -0400 Subject: [PATCH 39/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 be878918b02..9c8e7b69dca 100644 --- a/netbox/extras/middleware.py +++ b/netbox/extras/middleware.py @@ -29,10 +29,6 @@ def cache_changed_object(instance, **kwargs): def _record_object_deleted(request, instance, **kwargs): - # Force resolution of request.user in case it's still a SimpleLazyObject. This seems to happen - # occasionally during tests, but haven't been able to determine why. - assert request.user.is_authenticated - # Record that the object was deleted if hasattr(instance, 'log_change'): instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE) @@ -47,7 +43,7 @@ class ObjectChangeMiddleware(object): 1. Create an ObjectChange to reflect the modification to the object in the changelog. 2. Enqueue any relevant webhooks. - The post_save and pre_delete signals are employed to catch object modifications, however changes are recorded a bit + The post_save and post_delete signals are employed to catch object modifications, however changes are recorded a bit differently for each. Objects being saved are cached into thread-local storage for action *after* the response has completed. This ensures that serialization of the object is performed only after any related objects (e.g. tags) have been created. Conversely, deletions are acted upon immediately, so that the serialized representation of the @@ -65,10 +61,10 @@ class ObjectChangeMiddleware(object): # the same request. request.id = uuid.uuid4() - # Signals don't include the request context, so we're currying it into the pre_delete function ahead of time. + # Signals don't include the request context, so we're currying it into the post_delete function ahead of time. record_object_deleted = curry(_record_object_deleted, request) - # Connect our receivers to the post_save and pre_delete signals. + # Connect our receivers to the post_save and post_delete signals. post_save.connect(cache_changed_object, dispatch_uid='record_object_saved') post_delete.connect(record_object_deleted, dispatch_uid='record_object_deleted') From 7d053f8ba4e723b2de503d2a24bec89c6dd192d1 Mon Sep 17 00:00:00 2001 From: dansheps Date: Thu, 30 May 2019 10:54:29 -0500 Subject: [PATCH 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 | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b70c1dde6a7..d97ea98f31a 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 4e321ab1974..0721d0b8c2c 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -2,6 +2,7 @@ from django.conf import settings from django.db import ProgrammingError from django.http import Http404, HttpResponseRedirect from django.urls import reverse +import urllib from .views import server_error @@ -22,7 +23,7 @@ class LoginRequiredMiddleware(object): # performs its own authentication. api_path = reverse('api-root') if not request.path_info.startswith(api_path) and request.path_info != settings.LOGIN_URL: - return HttpResponseRedirect('{}?next={}'.format(settings.LOGIN_URL, request.path_info)) + return HttpResponseRedirect('{}?next={}'.format(settings.LOGIN_URL, urllib.parse.quote(request.get_full_path_info())) return self.get_response(request) From a11b33d2144730f451f8fbbebd35c89437a89524 Mon Sep 17 00:00:00 2001 From: dansheps Date: Thu, 30 May 2019 10:54:29 -0500 Subject: [PATCH 41/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 b70c1dde6a7..d97ea98f31a 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 4e321ab1974..c70be72ed2f 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -2,6 +2,7 @@ from django.conf import settings from django.db import ProgrammingError from django.http import Http404, HttpResponseRedirect from django.urls import reverse +import urllib from .views import server_error @@ -22,7 +23,8 @@ class LoginRequiredMiddleware(object): # performs its own authentication. api_path = reverse('api-root') if not request.path_info.startswith(api_path) and request.path_info != settings.LOGIN_URL: - return HttpResponseRedirect('{}?next={}'.format(settings.LOGIN_URL, request.path_info)) + return HttpResponseRedirect('{}?next={}'.format(settings.LOGIN_URL, + urllib.parse.quote(request.get_full_path_info()))) return self.get_response(request) From 814c50f46101deb77e9bf2b71c4b1663a6ed953b Mon Sep 17 00:00:00 2001 From: dansheps Date: Thu, 30 May 2019 12:01:41 -0500 Subject: [PATCH 42/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 5f8f371d3b0..177e5df589f 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 %}