From f78c228c757832abd7fabe41fa90ecd1d9eda3ea Mon Sep 17 00:00:00 2001 From: dansheps Date: Sat, 23 Feb 2019 10:37:30 -0600 Subject: [PATCH 001/105] Fixes #2813: Add Filter for TenantGroup to the following Forms and Filter classes: * circuit.Circuit * dcim.Site * dcim.Rack * dcim.RackElevation * dcim.RackReservation * dcim.Device * ipam.IPAddress * ipam.Prefix * ipam.VRF * ipam.VLAN * virtualization.VirtualMachine --- netbox/circuits/filters.py | 14 ++++++++- netbox/circuits/forms.py | 15 ++++++++- netbox/dcim/filters.py | 50 ++++++++++++++++++++++++++++- netbox/dcim/forms.py | 54 +++++++++++++++++++++++++++++++- netbox/ipam/filters.py | 50 ++++++++++++++++++++++++++++- netbox/ipam/forms.py | 54 +++++++++++++++++++++++++++++++- netbox/virtualization/filters.py | 14 ++++++++- netbox/virtualization/forms.py | 15 ++++++++- 8 files changed, 258 insertions(+), 8 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 12955eeca..f970c828e 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -3,7 +3,7 @@ from django.db.models import Q from dcim.models import Site from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from .constants import CIRCUIT_STATUS_CHOICES from .models import Provider, Circuit, CircuitTermination, CircuitType @@ -87,6 +87,18 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): choices=CIRCUIT_STATUS_CHOICES, null_value=None ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 4deee57c9..2a508b3c2 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -4,7 +4,7 @@ from taggit.forms import TagField from dcim.models import Site from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from tenancy.forms import TenancyForm -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple @@ -292,6 +292,19 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, widget=StaticSelect2Multiple() ) + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + null_option=True, + filter_for={ + 'tenant': 'group' + } + ) + ) tenant = FilterChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 96ecefafd..4710800c5 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -6,7 +6,7 @@ from netaddr import EUI from netaddr.core import AddrFormatError from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.constants import COLOR_CHOICES from utilities.filters import NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter from virtualization.models import Cluster @@ -59,6 +59,18 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): field_name='slug', label='Region (slug)', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', @@ -160,6 +172,18 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Group', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', @@ -241,6 +265,18 @@ class RackReservationFilter(django_filters.FilterSet): to_field_name='slug', label='Group', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', @@ -491,6 +527,18 @@ class DeviceFilter(CustomFieldFilterSet): to_field_name='slug', label='Role (slug)', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index ad209c516..9c7060bd7 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -13,7 +13,7 @@ from timezone_field import TimeZoneFormField from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from ipam.models import IPAddress, VLAN, VLANGroup from tenancy.forms import TenancyForm -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, @@ -276,6 +276,19 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): value_field="slug", ) ) + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + null_option=True, + filter_for={ + 'tenant': 'group' + } + ) + ) tenant = FilterChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', @@ -619,6 +632,19 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + null_option=True, + filter_for={ + 'tenant': 'group' + } + ) + ) tenant = FilterChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', @@ -711,6 +737,19 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form): null_option=True, ) ) + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + null_option=True, + filter_for={ + 'tenant': 'group' + } + ) + ) tenant = FilterChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', @@ -1703,6 +1742,19 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + null_option=True, + filter_for={ + 'tenant': 'group' + } + ) + ) tenant = FilterChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index f7125ceb0..9e6d4006e 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -6,7 +6,7 @@ from netaddr.core import AddrFormatError from dcim.models import Site, Device, Interface from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from virtualization.models import VirtualMachine from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES @@ -22,6 +22,18 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): method='search', label='Search', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', @@ -146,6 +158,18 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='rd', label='VRF (RD)', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', @@ -285,6 +309,18 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='rd', label='VRF (RD)', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', @@ -423,6 +459,18 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Group', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index d0e25f580..8bc207527 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -6,7 +6,7 @@ from taggit.forms import TagField from dcim.models import Site, Rack, Device, Interface from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from tenancy.forms import TenancyForm -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm, SlugField, @@ -103,6 +103,19 @@ class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + null_option=True, + filter_for={ + 'tenant': 'group' + } + ) + ) tenant = FilterChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', @@ -535,6 +548,19 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + null_option=True, + filter_for={ + 'tenant': 'group' + } + ) + ) tenant = FilterChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', @@ -984,6 +1010,19 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + null_option=True, + filter_for={ + 'tenant': 'group' + } + ) + ) tenant = FilterChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', @@ -1250,6 +1289,19 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + null_option=True, + filter_for={ + 'tenant': 'group' + } + ) + ) tenant = FilterChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 0b7e57ba7..32af27adc 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -6,7 +6,7 @@ from netaddr.core import AddrFormatError from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -150,6 +150,18 @@ class VirtualMachineFilter(CustomFieldFilterSet): to_field_name='slug', label='Role (slug)', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 70bbf0910..931dccc5d 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -8,7 +8,7 @@ from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, S from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm from ipam.models import IPAddress from tenancy.forms import TenancyForm -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm, @@ -591,6 +591,19 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, widget=StaticSelect2Multiple() ) + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + null_option=True, + filter_for={ + 'tenant': 'group' + } + ) + ) tenant = FilterChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', From 8683efe54ae438b589fcd9fdb580956d4024abef Mon Sep 17 00:00:00 2001 From: dansheps Date: Sat, 23 Feb 2019 11:09:02 -0600 Subject: [PATCH 002/105] Fixes #2813: Add Filter and List View for TenantGroup Added Filter for TenantGroup to the following Forms and Filter classes * circuit.Circuit * dcim.Site * dcim.Rack * dcim.RackElevation * dcim.RackReservation * dcim.Device * ipam.IPAddress * ipam.Prefix * ipam.VRF * ipam.VLAN * virtualization.VirtualMachine Added List View to the following classes: * circuit.Circuit * dcim.Site * dcim.Rack * dcim.RackReservation * dcim.Device * ipam.IPAddress * ipam.Prefix * ipam.VRF * ipam.VLAN * virtualization.VirtualMachine --- netbox/circuits/tables.py | 4 ++-- netbox/dcim/tables.py | 10 +++++----- netbox/ipam/tables.py | 15 +++++++++++---- netbox/tenancy/tables.py | 10 ++++++++++ netbox/virtualization/tables.py | 4 ++-- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index c6a215db8..f90a761a7 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django.utils.safestring import mark_safe from django_tables2.utils import Accessor -from tenancy.tables import COL_TENANT +from tenancy.tables import COL_TENANTGROUP_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import Circuit, CircuitType, Provider @@ -76,7 +76,7 @@ class CircuitTable(BaseTable): cid = tables.LinkColumn(verbose_name='ID') provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')]) status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side') termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side') diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 5649c10ef..11dcca81a 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from tenancy.tables import COL_TENANT +from tenancy.tables import COL_TENANT, COL_TENANTGROUP_TENANT from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -214,7 +214,7 @@ class SiteTable(BaseTable): name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3')) status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') region = tables.TemplateColumn(template_code=SITE_REGION_LINK) - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) class Meta(BaseTable.Meta): model = Site @@ -275,7 +275,7 @@ class RackTable(BaseTable): name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3')) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) status = tables.TemplateColumn(STATUS_LABEL) role = tables.TemplateColumn(RACK_ROLE) u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') @@ -305,7 +305,7 @@ class RackDetailTable(RackTable): class RackReservationTable(BaseTable): pk = ToggleColumn() - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) unit_list = tables.Column(orderable=False, verbose_name='Units') actions = tables.TemplateColumn( @@ -512,7 +512,7 @@ class DeviceTable(BaseTable): template_code=DEVICE_LINK ) status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role') diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 026cbc980..2ae9d562a 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Interface -from tenancy.tables import COL_TENANT +from tenancy.tables import COL_TENANT,COL_TENANTGROUP_TENANT from utilities.tables import BaseTable, BooleanColumn, ToggleColumn from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF @@ -169,8 +169,12 @@ VLAN_MEMBER_ACTIONS = """ """ TENANT_LINK = """ -{% if record.tenant %} +{% if record.tenant and record.tenant.group %} + {{record.tenant.group}}:{{ record.tenant }} +{% elif record.tenant %} {{ record.tenant }} +{% elif record.vrf.tenant.group %} + {{record.vrf.tenant.group}}:{{ record.vrf.tenant }}* {% elif record.vrf.tenant %} {{ record.vrf.tenant }}* {% else %} @@ -187,7 +191,7 @@ class VRFTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() rd = tables.Column(verbose_name='RD') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) class Meta(BaseTable.Meta): model = VRF @@ -319,6 +323,7 @@ class PrefixTable(BaseTable): class PrefixDetailTable(PrefixTable): utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) class Meta(PrefixTable.Meta): fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description') @@ -349,6 +354,7 @@ class IPAddressDetailTable(IPAddressTable): nat_inside = tables.LinkColumn( 'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)' ) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) class Meta(IPAddressTable.Meta): fields = ( @@ -409,7 +415,7 @@ class VLANTable(BaseTable): vid = tables.TemplateColumn(VLAN_LINK, verbose_name='ID') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.LinkColumn('ipam:vlangroup_vlans', args=[Accessor('group.pk')], verbose_name='Group') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) status = tables.TemplateColumn(STATUS_LABEL) role = tables.TemplateColumn(VLAN_ROLE_LINK) @@ -423,6 +429,7 @@ class VLANTable(BaseTable): class VLANDetailTable(VLANTable): prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes') + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) class Meta(VLANTable.Meta): fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description') diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 91122df7a..779d3bd2e 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -20,6 +20,16 @@ COL_TENANT = """ {% endif %} """ +COL_TENANTGROUP_TENANT = """ +{% if record.tenant and record.tenant.group %} + {{record.tenant.group}}:{{ record.tenant }} +{% elif record.tenant %} + {{ record.tenant }} +{% else %} + — +{% endif %} +""" + # # Tenant groups diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index b825ba59f..354f9025d 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Interface -from tenancy.tables import COL_TENANT +from tenancy.tables import COL_TENANTGROUP_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -103,7 +103,7 @@ class VirtualMachineTable(BaseTable): status = tables.TemplateColumn(template_code=VIRTUALMACHINE_STATUS) cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')]) role = tables.TemplateColumn(VIRTUALMACHINE_ROLE) - tenant = tables.TemplateColumn(template_code=COL_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) class Meta(BaseTable.Meta): model = VirtualMachine From 679aa0f764d6b0c830d093cd59c9a753757c49e0 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 26 Feb 2019 07:53:59 -0600 Subject: [PATCH 003/105] Update tables.py Fix whitespace --- netbox/ipam/tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 2ae9d562a..ff8cf8928 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Interface -from tenancy.tables import COL_TENANT,COL_TENANTGROUP_TENANT +from tenancy.tables import COL_TENANT, COL_TENANTGROUP_TENANT from utilities.tables import BaseTable, BooleanColumn, ToggleColumn from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF From f2471aedb28b232f926ec3f43dddb981d0d404f9 Mon Sep 17 00:00:00 2001 From: dansheps Date: Wed, 27 Feb 2019 11:39:25 -0600 Subject: [PATCH 004/105] Fixes #2781: Fixes filter by regions on site and device list --- netbox/dcim/filters.py | 17 ++++++++++------- netbox/utilities/filters.py | 9 +++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 96ecefafd..b8086322d 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -8,7 +8,9 @@ from netaddr.core import AddrFormatError from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant from utilities.constants import COLOR_CHOICES -from utilities.filters import NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter +from utilities.filters import ( + NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter +) from virtualization.models import Cluster from .constants import * from .models import ( @@ -49,14 +51,15 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): choices=SITE_STATUS_CHOICES, null_value=None ) - region_id = django_filters.NumberFilter( - method='filter_region', - field_name='pk', + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='region__in', label='Region (ID)', ) - region = django_filters.CharFilter( - method='filter_region', - field_name='slug', + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='region__in', + to_field_name='slug', label='Region (slug)', ) tenant_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py index 40e687077..b0c2b3ec3 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -4,6 +4,15 @@ from django.db.models import Q from taggit.models import Tag +class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter): + """ + Filters for a set of Models, including all descendant models within a Tree. Example: [,] + """ + def filter(self, qs, value): + value = [node.get_descendants(include_self=True) for node in value] + return super().filter(qs, value) + + class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter): """ Filters for a set of numeric values. Example: id__in=100,200,300 From 00aaf500de1ac67bcba46fbcc75d9ea51cf6ddca Mon Sep 17 00:00:00 2001 From: dansheps Date: Wed, 27 Feb 2019 14:46:11 -0600 Subject: [PATCH 005/105] Fixes #2781: Fixes filter by regions on site and device list * Add Device filter --- netbox/dcim/filters.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index b8086322d..d06a65ad3 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -98,16 +98,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): pass return queryset.filter(qs_filter) - def filter_region(self, queryset, name, value): - try: - region = Region.objects.get(**{name: value}) - except ObjectDoesNotExist: - return queryset.none() - return queryset.filter( - Q(region=region) | - Q(region__in=region.get_descendants()) - ) - class RackGroupFilter(NameSlugSearchFilterSet): site_id = django_filters.ModelMultipleChoiceFilter( @@ -516,14 +506,15 @@ class DeviceFilter(CustomFieldFilterSet): ) name = NullableCharFieldFilter() asset_tag = NullableCharFieldFilter() - region_id = django_filters.NumberFilter( - method='filter_region', - field_name='pk', + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='region__in', label='Region (ID)', ) - region = django_filters.CharFilter( - method='filter_region', - field_name='slug', + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='region__in', + to_field_name='slug', label='Region (slug)', ) site_id = django_filters.ModelMultipleChoiceFilter( From b381bdec271167641718c472b78167e50622454f Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 3 Mar 2019 18:52:57 -0500 Subject: [PATCH 006/105] fixes #2952 - slug field absent from TenantFilter --- CHANGELOG.md | 1 + netbox/tenancy/filters.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82949ff16..82df28edc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ v2.5.8 (FUTURE) * [#2923](https://github.com/digitalocean/netbox/issues/2923) - Provider filter form's site field should be blank by default * [#2938](https://github.com/digitalocean/netbox/issues/2938) - Enforce deterministic ordering of device components returned by API * [#2939](https://github.com/digitalocean/netbox/issues/2939) - Exclude circuit terminations from API interface connections endpoint +* [#2952](https://github.com/digitalocean/netbox/issues/2952) - Added the `slug` field to the Tenant filter for use in the API and search function --- diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index 745391898..2610b3ec0 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -36,13 +36,14 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet): class Meta: model = Tenant - fields = ['name'] + fields = ['name', 'slug'] def search(self, queryset, name, value): if not value.strip(): return queryset return queryset.filter( Q(name__icontains=value) | + Q(slug__icontains=value) | Q(description__icontains=value) | Q(comments__icontains=value) ) From 4d18d9661b641b60a7da94671bbd3cb625e036f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristi=20V=C3=AEjdea?= Date: Mon, 4 Mar 2019 03:20:47 +0200 Subject: [PATCH 007/105] Remove trailing slashes from filesystem paths Paths with trailing slashes do not work on windows, they cause errors such as `django.core.exceptions.SuspiciousFileOperation: The joined path (C:\Projects\netbox\netbox\static\clipboard-2.0.4.min.js) is located outside of the base path component (C:\Projects\netbox\netbox\static\)`. --- netbox/netbox/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index a85a5d78e..c2622998f 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -197,7 +197,7 @@ ROOT_URLCONF = 'netbox.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR + '/templates/'], + 'DIRS': [BASE_DIR + '/templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -223,7 +223,7 @@ USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) -STATIC_ROOT = BASE_DIR + '/static/' +STATIC_ROOT = BASE_DIR + '/static' STATIC_URL = '/{}static/'.format(BASE_PATH) STATICFILES_DIRS = ( os.path.join(BASE_DIR, "project-static"), From 3a62e9a3227d2899613f2873469b82a0dd488bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristi=20V=C3=AEjdea?= Date: Mon, 4 Mar 2019 03:24:45 +0200 Subject: [PATCH 008/105] Resolve drf-yasg `ref_name` conflicts This solves the problem of distinct serializers being confused because they have the same class name (e.g. `InterfaceSerializer`) Fixes #2065. --- netbox/utilities/custom_inspectors.py | 22 +++++++++++++++++++++- requirements.txt | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/netbox/utilities/custom_inspectors.py b/netbox/utilities/custom_inspectors.py index 5975788bc..577e3c202 100644 --- a/netbox/utilities/custom_inspectors.py +++ b/netbox/utilities/custom_inspectors.py @@ -1,14 +1,24 @@ from drf_yasg import openapi from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, FilterInspector, SwaggerAutoSchema +from drf_yasg.utils import get_serializer_ref_name from rest_framework.fields import ChoiceField from rest_framework.relations import ManyRelatedField from taggit_serializer.serializers import TagListSerializerField +from dcim.api.serializers import InterfaceSerializer as DCIMInterfaceSerializer +from virtualization.api.serializers import InterfaceSerializer as VirtualMachineInterfaceSerializer from extras.api.customfields import CustomFieldsSerializer from utilities.api import ChoiceField, SerializedPKRelatedField, WritableNestedSerializer +# this might be ugly, but it limits drf_yasg-specific code to this file +DCIMInterfaceSerializer.Meta.ref_name = 'DCIMInterface' +VirtualMachineInterfaceSerializer.Meta.ref_name = 'VirtualMachineInterface' + + class NetBoxSwaggerAutoSchema(SwaggerAutoSchema): + writable_serializers = {} + def get_request_serializer(self): serializer = super().get_request_serializer() @@ -21,7 +31,17 @@ class NetBoxSwaggerAutoSchema(SwaggerAutoSchema): properties[child_name] = None if properties: - writable_class = type('Writable' + type(serializer).__name__, (type(serializer),), properties) + if type(serializer) not in self.writable_serializers: + writable_name = 'Writable' + type(serializer).__name__ + meta_class = getattr(type(serializer), 'Meta', None) + if meta_class: + ref_name = 'Writable' + get_serializer_ref_name(serializer) + writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name}) + properties['Meta'] = writable_meta + + self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties) + + writable_class = self.writable_serializers[type(serializer)] serializer = writable_class() return serializer diff --git a/requirements.txt b/requirements.txt index e313e9a69..49e7cf39e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ django-taggit==0.23.0 django-taggit-serializer==0.1.7 django-timezone-field==3.0 djangorestframework==3.9.0 -drf-yasg[validation]==1.11.1 +drf-yasg[validation]==1.14.0 graphviz==0.10.1 Markdown==2.6.11 netaddr==0.7.19 From 78725b8483a6920620f5125ccb8fdd2a07d48218 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2019 14:57:35 -0500 Subject: [PATCH 009/105] Follow-up from #2781 --- CHANGELOG.md | 1 + netbox/dcim/filters.py | 15 ++------------- netbox/virtualization/filters.py | 25 ++++++++----------------- 3 files changed, 11 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82df28edc..a2acd88dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ v2.5.8 (FUTURE) ## Bug Fixes * [#2705](https://github.com/digitalocean/netbox/issues/2705) - Fix endpoint grouping in API docs +* [#2781](https://github.com/digitalocean/netbox/issues/2781) - Fix filtering of sites/devices/VMs by multiple regions * [#2923](https://github.com/digitalocean/netbox/issues/2923) - Provider filter form's site field should be blank by default * [#2938](https://github.com/digitalocean/netbox/issues/2938) - Enforce deterministic ordering of device components returned by API * [#2939](https://github.com/digitalocean/netbox/issues/2939) - Exclude circuit terminations from API interface connections endpoint diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index d06a65ad3..dda904f1c 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,6 +1,5 @@ import django_filters from django.contrib.auth.models import User -from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from netaddr import EUI from netaddr.core import AddrFormatError @@ -508,12 +507,12 @@ class DeviceFilter(CustomFieldFilterSet): asset_tag = NullableCharFieldFilter() region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='region__in', + field_name='site__region__in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='region__in', + field_name='site__region__in', to_field_name='slug', label='Region (slug)', ) @@ -613,16 +612,6 @@ class DeviceFilter(CustomFieldFilterSet): Q(comments__icontains=value) ).distinct() - def filter_region(self, queryset, name, value): - try: - region = Region.objects.get(**{name: value}) - except ObjectDoesNotExist: - return queryset.none() - return queryset.filter( - Q(site__region=region) | - Q(site__region__in=region.get_descendants()) - ) - def _mac_address(self, queryset, name, value): value = value.strip() if not value: diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 0b7e57ba7..0e5ff6cd2 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -7,7 +7,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 utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter +from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -119,14 +119,15 @@ class VirtualMachineFilter(CustomFieldFilterSet): queryset=Cluster.objects.all(), label='Cluster (ID)', ) - region_id = django_filters.NumberFilter( - method='filter_region', - field_name='pk', + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='cluster__site__region__in', label='Region (ID)', ) - region = django_filters.CharFilter( - method='filter_region', - field_name='slug', + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='cluster__site__region__in', + to_field_name='slug', label='Region (slug)', ) site_id = django_filters.ModelMultipleChoiceFilter( @@ -184,16 +185,6 @@ class VirtualMachineFilter(CustomFieldFilterSet): Q(comments__icontains=value) ) - def filter_region(self, queryset, name, value): - try: - region = Region.objects.get(**{name: value}) - except ObjectDoesNotExist: - return queryset.none() - return queryset.filter( - Q(cluster__site__region=region) | - Q(cluster__site__region__in=region.get_descendants()) - ) - class InterfaceFilter(django_filters.FilterSet): q = django_filters.CharFilter( From 0c142f207832cb563bed654fd21bcda0bb7aff59 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2019 15:16:35 -0500 Subject: [PATCH 010/105] Changelog for #2954 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2acd88dc..d96ca2ad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ v2.5.8 (FUTURE) * [#2938](https://github.com/digitalocean/netbox/issues/2938) - Enforce deterministic ordering of device components returned by API * [#2939](https://github.com/digitalocean/netbox/issues/2939) - Exclude circuit terminations from API interface connections endpoint * [#2952](https://github.com/digitalocean/netbox/issues/2952) - Added the `slug` field to the Tenant filter for use in the API and search function +* [#2954](https://github.com/digitalocean/netbox/issues/2954) - Remove trailing slashes to fix root/template paths on Windows --- From b4d7f9ea43564567b19a8dbe203d1b21d3f64aff Mon Sep 17 00:00:00 2001 From: dansheps Date: Tue, 5 Mar 2019 08:10:10 -0600 Subject: [PATCH 011/105] Fixes #2781: Fixes filter by regions on site and device list * Add Device filter --- netbox/circuits/filters.py | 26 +------ netbox/circuits/forms.py | 31 ++------- netbox/circuits/tables.py | 4 +- netbox/dcim/filters.py | 111 ++---------------------------- netbox/dcim/forms.py | 112 ++++--------------------------- netbox/dcim/tables.py | 10 +-- netbox/ipam/filters.py | 98 ++------------------------- netbox/ipam/forms.py | 112 ++++--------------------------- netbox/ipam/tables.py | 12 ++-- netbox/tenancy/filters.py | 25 +++++++ netbox/tenancy/forms.py | 28 ++++++++ netbox/virtualization/filters.py | 26 +------ netbox/virtualization/forms.py | 31 ++------- netbox/virtualization/tables.py | 4 +- 14 files changed, 121 insertions(+), 509 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index f970c828e..f54567f68 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -3,7 +3,7 @@ from django.db.models import Q from dcim.models import Site from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant, TenantGroup +from tenancy.filters import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from .constants import CIRCUIT_STATUS_CHOICES from .models import Provider, Circuit, CircuitTermination, CircuitType @@ -54,7 +54,7 @@ class CircuitTypeFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): +class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -87,28 +87,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): choices=CIRCUIT_STATUS_CHOICES, null_value=None ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (slug)', - ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) site_id = django_filters.ModelMultipleChoiceFilter( field_name='terminations__site', queryset=Site.objects.all(), diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 2a508b3c2..95237ab1c 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -3,8 +3,8 @@ from taggit.forms import TagField from dcim.models import Site from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm -from tenancy.forms import TenancyForm -from tenancy.models import Tenant, TenantGroup +from tenancy.forms import TenancyForm, TenancyFilterForm +from tenancy.models import Tenant from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple @@ -265,8 +265,10 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit ] -class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): +class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Circuit + # Order the form fields, fields not listed are appended + field_order = ['q', 'type', 'provider', 'status'] q = forms.CharField( required=False, label='Search' @@ -292,29 +294,6 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, widget=StaticSelect2Multiple() ) - tenant_group = FilterChoiceField( - queryset=TenantGroup.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenant-groups/", - value_field="slug", - null_option=True, - filter_for={ - 'tenant': 'group' - } - ) - ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index f90a761a7..c6a215db8 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django.utils.safestring import mark_safe from django_tables2.utils import Accessor -from tenancy.tables import COL_TENANTGROUP_TENANT +from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import Circuit, CircuitType, Provider @@ -76,7 +76,7 @@ class CircuitTable(BaseTable): cid = tables.LinkColumn(verbose_name='ID') provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')]) status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side') termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side') diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 4710800c5..7d609426d 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -6,7 +6,7 @@ from netaddr import EUI from netaddr.core import AddrFormatError from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant, TenantGroup +from tenancy.filters import TenancyFilterSet from utilities.constants import COLOR_CHOICES from utilities.filters import NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter from virtualization.models import Cluster @@ -36,7 +36,7 @@ class RegionFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): +class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -59,28 +59,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): field_name='slug', label='Region (slug)', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (slug)', - ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) tag = TagFilter() class Meta: @@ -142,7 +120,7 @@ class RackRoleFilter(NameSlugSearchFilterSet): fields = ['name', 'slug', 'color'] -class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): +class RackFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -172,28 +150,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Group', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (slug)', - ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) status = django_filters.MultipleChoiceFilter( choices=RACK_STATUS_CHOICES, null_value=None @@ -230,7 +186,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): ) -class RackReservationFilter(django_filters.FilterSet): +class RackReservationFilter(TenancyFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -265,28 +221,6 @@ class RackReservationFilter(django_filters.FilterSet): to_field_name='slug', label='Group', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (slug)', - ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) user_id = django_filters.ModelMultipleChoiceFilter( queryset=User.objects.all(), label='User (ID)', @@ -492,7 +426,7 @@ class PlatformFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class DeviceFilter(CustomFieldFilterSet): +class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -527,28 +461,6 @@ class DeviceFilter(CustomFieldFilterSet): to_field_name='slug', label='Role (slug)', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (slug)', - ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) platform_id = django_filters.ModelMultipleChoiceFilter( queryset=Platform.objects.all(), label='Platform (ID)', @@ -978,7 +890,7 @@ class InventoryItemFilter(DeviceComponentFilterSet): return queryset.filter(qs_filter) -class VirtualChassisFilter(django_filters.FilterSet): +class VirtualChassisFilter(TenancyFilterSet, django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -994,17 +906,6 @@ class VirtualChassisFilter(django_filters.FilterSet): to_field_name='slug', label='Site name (slug)', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - field_name='master__tenant', - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='master__tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) tag = TagFilter() class Meta: diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 9c7060bd7..f851e0938 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -12,8 +12,8 @@ from timezone_field import TimeZoneFormField from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from ipam.models import IPAddress, VLAN, VLANGroup -from tenancy.forms import TenancyForm -from tenancy.models import Tenant, TenantGroup +from tenancy.forms import TenancyForm, TenancyFilterForm +from tenancy.models import Tenant from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, @@ -256,8 +256,10 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor ] -class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): +class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Site + # Order the form fields, fields not listed are appended + field_order = ['q', 'status', 'region'] q = forms.CharField( required=False, label='Search' @@ -276,29 +278,6 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): value_field="slug", ) ) - tenant_group = FilterChoiceField( - queryset=TenantGroup.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenant-groups/", - value_field="slug", - null_option=True, - filter_for={ - 'tenant': 'group' - } - ) - ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) # @@ -609,8 +588,10 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor ] -class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): +class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Rack + # Order the form fields, fields not listed are appended + field_order = ['q', 'site', 'group_id'] q = forms.CharField( required=False, label='Search' @@ -632,29 +613,6 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - tenant_group = FilterChoiceField( - queryset=TenantGroup.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenant-groups/", - value_field="slug", - null_option=True, - filter_for={ - 'tenant': 'group' - } - ) - ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) status = forms.MultipleChoiceField( choices=RACK_STATUS_CHOICES, required=False, @@ -715,7 +673,9 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm): return unit_choices -class RackReservationFilterForm(BootstrapMixin, forms.Form): +class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, forms.Form): + # Order the form fields, fields not listed are appended + field_order = ['q', 'site', 'group_id'] q = forms.CharField( required=False, label='Search' @@ -737,29 +697,6 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form): null_option=True, ) ) - tenant_group = FilterChoiceField( - queryset=TenantGroup.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenant-groups/", - value_field="slug", - null_option=True, - filter_for={ - 'tenant': 'group' - } - ) - ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): @@ -1682,8 +1619,10 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF ] -class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): +class DeviceFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Device + # Order the form fields, fields not listed are appended + field_order = ['q', 'region', 'site', 'rack_group_id', 'rack_id', 'role'] q = forms.CharField( required=False, label='Search' @@ -1742,29 +1681,6 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - tenant_group = FilterChoiceField( - queryset=TenantGroup.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenant-groups/", - value_field="slug", - null_option=True, - filter_for={ - 'tenant': 'group' - } - ) - ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) manufacturer_id = FilterChoiceField( queryset=Manufacturer.objects.all(), label='Manufacturer', diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 11dcca81a..b61bfed64 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from tenancy.tables import COL_TENANT, COL_TENANTGROUP_TENANT +from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -214,7 +214,7 @@ class SiteTable(BaseTable): name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3')) status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') region = tables.TemplateColumn(template_code=SITE_REGION_LINK) - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(BaseTable.Meta): model = Site @@ -275,7 +275,7 @@ class RackTable(BaseTable): name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3')) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) status = tables.TemplateColumn(STATUS_LABEL) role = tables.TemplateColumn(RACK_ROLE) u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') @@ -305,7 +305,7 @@ class RackDetailTable(RackTable): class RackReservationTable(BaseTable): pk = ToggleColumn() - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) unit_list = tables.Column(orderable=False, verbose_name='Units') actions = tables.TemplateColumn( @@ -512,7 +512,7 @@ class DeviceTable(BaseTable): template_code=DEVICE_LINK ) status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role') diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 9e6d4006e..da60c9f3d 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -6,14 +6,14 @@ from netaddr.core import AddrFormatError from dcim.models import Site, Device, Interface from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant, TenantGroup +from tenancy.filters import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from virtualization.models import VirtualMachine from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF -class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): +class VRFFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -22,28 +22,6 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): method='search', label='Search', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (slug)', - ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) tag = TagFilter() def search(self, queryset, name, value): @@ -119,7 +97,7 @@ class RoleFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): +class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -158,28 +136,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='rd', label='VRF (RD)', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (slug)', - ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -278,7 +234,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): return queryset.filter(prefix__net_mask_length=value) -class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): +class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -309,28 +265,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='rd', label='VRF (RD)', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (slug)', - ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) device = django_filters.CharFilter( method='filter_device', field_name='name', @@ -430,7 +364,7 @@ class VLANGroupFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): +class VLANFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -459,28 +393,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Group', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (slug)', - ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) role_id = django_filters.ModelMultipleChoiceFilter( queryset=Role.objects.all(), label='Role (ID)', diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 8bc207527..6b97c1f83 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -5,8 +5,8 @@ from taggit.forms import TagField from dcim.models import Site, Rack, Device, Interface from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm -from tenancy.forms import TenancyForm -from tenancy.models import Tenant, TenantGroup +from tenancy.forms import TenancyForm, TenancyFilterForm +from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm, SlugField, @@ -97,35 +97,14 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm ] -class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm): +class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VRF + # Order the form fields, fields not listed are appended + field_order = ['q'] q = forms.CharField( required=False, label='Search' ) - tenant_group = FilterChoiceField( - queryset=TenantGroup.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenant-groups/", - value_field="slug", - null_option=True, - filter_for={ - 'tenant': 'group' - } - ) - ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) # @@ -510,8 +489,10 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF ] -class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): +class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Prefix + # Order the form fields, fields not listed are appended + field_order = ['q', 'within_include', 'family', 'mask_length', 'vrf'] q = forms.CharField( required=False, label='Search' @@ -548,29 +529,6 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - tenant_group = FilterChoiceField( - queryset=TenantGroup.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenant-groups/", - value_field="slug", - null_option=True, - filter_for={ - 'tenant': 'group' - } - ) - ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) status = forms.MultipleChoiceField( choices=PREFIX_STATUS_CHOICES, required=False, @@ -972,8 +930,10 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form): ) -class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): +class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = IPAddress + # Order the form fields, fields not listed are appended + field_order = ['q', 'parent', 'family', 'mask_length', 'vrf'] q = forms.CharField( required=False, label='Search' @@ -1010,29 +970,6 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - tenant_group = FilterChoiceField( - queryset=TenantGroup.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenant-groups/", - value_field="slug", - null_option=True, - filter_for={ - 'tenant': 'group' - } - ) - ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) status = forms.MultipleChoiceField( choices=IPADDRESS_STATUS_CHOICES, required=False, @@ -1264,8 +1201,10 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor ] -class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): +class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VLAN + # Order the form fields, fields not listed are appended + field_order = ['q', 'site', 'group_id'] q = forms.CharField( required=False, label='Search' @@ -1289,29 +1228,6 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - tenant_group = FilterChoiceField( - queryset=TenantGroup.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenant-groups/", - value_field="slug", - null_option=True, - filter_for={ - 'tenant': 'group' - } - ) - ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenants/", - value_field="slug", - null_option=True, - ) - ) status = forms.MultipleChoiceField( choices=VLAN_STATUS_CHOICES, required=False, diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 2ae9d562a..7a5ad97c3 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Interface -from tenancy.tables import COL_TENANT,COL_TENANTGROUP_TENANT +from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, BooleanColumn, ToggleColumn from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF @@ -191,7 +191,7 @@ class VRFTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() rd = tables.Column(verbose_name='RD') - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(BaseTable.Meta): model = VRF @@ -323,7 +323,7 @@ class PrefixTable(BaseTable): class PrefixDetailTable(PrefixTable): utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False) - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(PrefixTable.Meta): fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description') @@ -354,7 +354,7 @@ class IPAddressDetailTable(IPAddressTable): nat_inside = tables.LinkColumn( 'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)' ) - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(IPAddressTable.Meta): fields = ( @@ -415,7 +415,7 @@ class VLANTable(BaseTable): vid = tables.TemplateColumn(VLAN_LINK, verbose_name='ID') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.LinkColumn('ipam:vlangroup_vlans', args=[Accessor('group.pk')], verbose_name='Group') - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) status = tables.TemplateColumn(STATUS_LABEL) role = tables.TemplateColumn(VLAN_ROLE_LINK) @@ -429,7 +429,7 @@ class VLANTable(BaseTable): class VLANDetailTable(VLANTable): prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes') - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(VLANTable.Meta): fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description') diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index 745391898..a7bf19f59 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -46,3 +46,28 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet): Q(description__icontains=value) | Q(comments__icontains=value) ) + + +class TenancyFilterSet(django_filters.FilterSet): + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__id', + queryset=TenantGroup.objects.all(), + to_field_name='id', + label='Tenant Group (ID)', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__group__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant Group (slug)', + ) + tenant_id = django_filters.ModelMultipleChoiceFilter( + queryset=Tenant.objects.all(), + label='Tenant (ID)', + ) + tenant = django_filters.ModelMultipleChoiceFilter( + field_name='tenant__slug', + queryset=Tenant.objects.all(), + to_field_name='slug', + label='Tenant (slug)', + ) diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 3c97eb801..6e2c59cd4 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -115,6 +115,34 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): ) ) +# +# Tenancy filtering form extension +# +class TenancyFilterForm(forms.Form): + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + null_option=True, + filter_for={ + 'tenant': 'group' + } + ) + ) + tenant = FilterChoiceField( + queryset=Tenant.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenants/", + value_field="slug", + null_option=True, + ) + ) + # # Tenancy form extension diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 32af27adc..5224d11ba 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -6,7 +6,7 @@ from netaddr.core import AddrFormatError from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant, TenantGroup +from tenancy.filters import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -80,7 +80,7 @@ class ClusterFilter(CustomFieldFilterSet): ) -class VirtualMachineFilter(CustomFieldFilterSet): +class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -150,28 +150,6 @@ class VirtualMachineFilter(CustomFieldFilterSet): to_field_name='slug', label='Role (slug)', ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__id', - queryset=TenantGroup.objects.all(), - to_field_name='id', - label='Tenant Group (ID)', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__group__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant Group (slug)', - ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) platform_id = django_filters.ModelMultipleChoiceFilter( queryset=Platform.objects.all(), label='Platform (ID)', diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 931dccc5d..eb194e6b2 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -7,7 +7,7 @@ from dcim.forms import INTERFACE_MODE_HELP_TEXT from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm from ipam.models import IPAddress -from tenancy.forms import TenancyForm +from tenancy.forms import TenancyForm, TenancyFilterForm from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, @@ -336,7 +336,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): class Meta: model = VirtualMachine fields = [ - 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', + 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data', ] help_texts = { @@ -520,8 +520,10 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB ] -class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): +class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VirtualMachine + # Order the form fields, fields not listed are appended + field_order = ['q', 'cluster_group', 'cluster_type', 'cluster_id', 'region', 'site'] q = forms.CharField( required=False, label='Search' @@ -591,29 +593,6 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, widget=StaticSelect2Multiple() ) - tenant_group = FilterChoiceField( - queryset=TenantGroup.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/tenancy/tenant-groups/", - value_field="slug", - null_option=True, - filter_for={ - 'tenant': 'group' - } - ) - ) - tenant = FilterChoiceField( - queryset=Tenant.objects.all(), - to_field_name='slug', - null_label='-- None --', - widget=APISelectMultiple( - api_url='/api/tenancy/tenants/', - value_field="slug", - null_option=True, - ) - ) platform = FilterChoiceField( queryset=Platform.objects.all(), to_field_name='slug', diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index 354f9025d..b825ba59f 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Interface -from tenancy.tables import COL_TENANTGROUP_TENANT +from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -103,7 +103,7 @@ class VirtualMachineTable(BaseTable): status = tables.TemplateColumn(template_code=VIRTUALMACHINE_STATUS) cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')]) role = tables.TemplateColumn(VIRTUALMACHINE_ROLE) - tenant = tables.TemplateColumn(template_code=COL_TENANTGROUP_TENANT) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(BaseTable.Meta): model = VirtualMachine From 37811d3f7e3c04d7d2828f7695975e133fd01cbd Mon Sep 17 00:00:00 2001 From: dansheps Date: Tue, 5 Mar 2019 08:19:21 -0600 Subject: [PATCH 012/105] * Resolve conflict with virtualization filters. --- netbox/virtualization/filters.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 0e5ff6cd2..06b8f8553 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -6,7 +6,7 @@ from netaddr.core import AddrFormatError from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet -from tenancy.models import Tenant +from tenancy.filters import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -80,7 +80,7 @@ class ClusterFilter(CustomFieldFilterSet): ) -class VirtualMachineFilter(CustomFieldFilterSet): +class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -151,16 +151,6 @@ class VirtualMachineFilter(CustomFieldFilterSet): to_field_name='slug', label='Role (slug)', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - queryset=Tenant.objects.all(), - label='Tenant (ID)', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenant__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) platform_id = django_filters.ModelMultipleChoiceFilter( queryset=Platform.objects.all(), label='Platform (ID)', From bd65e782bba0a1e84c96aa7257780d766c05648c Mon Sep 17 00:00:00 2001 From: Anthony Ruhier Date: Tue, 5 Mar 2019 18:44:44 +0100 Subject: [PATCH 013/105] Allow nullable length unit in cable API Cables models define it as None by default, but the API rejects a request containing a null length_unit. Allows it in the API serializer. --- netbox/dcim/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index c17400a35..4c65a3a19 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -507,7 +507,7 @@ class CableSerializer(ValidatedModelSerializer): termination_a = serializers.SerializerMethodField(read_only=True) termination_b = serializers.SerializerMethodField(read_only=True) status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False) - length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False) + length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False, allow_null=True) class Meta: model = Cable From cfb56f7cfee9ad64d3b272ee4f85e44091b8e506 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 5 Mar 2019 13:08:40 -0500 Subject: [PATCH 014/105] Fixes #2962: Increase ExportTemplate mime_type field length --- CHANGELOG.md | 1 + .../0017_exporttemplate_mime_type_length.py | 18 ++++++++++++++++++ netbox/extras/models.py | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 netbox/extras/migrations/0017_exporttemplate_mime_type_length.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d96ca2ad7..3a436d546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ v2.5.8 (FUTURE) * [#2939](https://github.com/digitalocean/netbox/issues/2939) - Exclude circuit terminations from API interface connections endpoint * [#2952](https://github.com/digitalocean/netbox/issues/2952) - Added the `slug` field to the Tenant filter for use in the API and search function * [#2954](https://github.com/digitalocean/netbox/issues/2954) - Remove trailing slashes to fix root/template paths on Windows +* [#2962](https://github.com/digitalocean/netbox/issues/2962) - Increase ExportTemplate `mime_type` field length --- diff --git a/netbox/extras/migrations/0017_exporttemplate_mime_type_length.py b/netbox/extras/migrations/0017_exporttemplate_mime_type_length.py new file mode 100644 index 000000000..29283e0d1 --- /dev/null +++ b/netbox/extras/migrations/0017_exporttemplate_mime_type_length.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.7 on 2019-03-05 18:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0016_exporttemplate_add_cable'), + ] + + operations = [ + migrations.AlterField( + model_name='exporttemplate', + name='mime_type', + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index d3b9f4eff..1b106a62a 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -357,7 +357,7 @@ class ExportTemplate(models.Model): ) template_code = models.TextField() mime_type = models.CharField( - max_length=15, + max_length=50, blank=True ) file_extension = models.CharField( From 6406e213bdcc0c5babf221d70293fe2270f78961 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 5 Mar 2019 13:15:09 -0500 Subject: [PATCH 015/105] Fixes #2961: Prevent exception when exporting inventory items belonging to unnamed devices --- CHANGELOG.md | 1 + netbox/dcim/models.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a436d546..89634c1c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ v2.5.8 (FUTURE) * [#2939](https://github.com/digitalocean/netbox/issues/2939) - Exclude circuit terminations from API interface connections endpoint * [#2952](https://github.com/digitalocean/netbox/issues/2952) - Added the `slug` field to the Tenant filter for use in the API and search function * [#2954](https://github.com/digitalocean/netbox/issues/2954) - Remove trailing slashes to fix root/template paths on Windows +* [#2961](https://github.com/digitalocean/netbox/issues/2961) - Prevent exception when exporting inventory items belonging to unnamed devices * [#2962](https://github.com/digitalocean/netbox/issues/2962) - Increase ExportTemplate `mime_type` field length --- diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 49879beb1..8133cbfb4 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -2423,7 +2423,7 @@ class InventoryItem(ComponentModel): def to_csv(self): return ( - self.device.name or '{' + self.device.pk + '}', + self.device.name or '{{{}}}'.format(self.device.pk), self.name, self.manufacturer.name if self.manufacturer else None, self.part_id, From 7294f43fa3601a59d4a80fcb35958290ffbfe1f7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 5 Mar 2019 13:18:56 -0500 Subject: [PATCH 016/105] Changelog for #2966 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89634c1c0..6a4e4b721 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ v2.5.8 (FUTURE) * [#2954](https://github.com/digitalocean/netbox/issues/2954) - Remove trailing slashes to fix root/template paths on Windows * [#2961](https://github.com/digitalocean/netbox/issues/2961) - Prevent exception when exporting inventory items belonging to unnamed devices * [#2962](https://github.com/digitalocean/netbox/issues/2962) - Increase ExportTemplate `mime_type` field length +* [#2966](https://github.com/digitalocean/netbox/issues/2966) - Accept `null` cable length_unit via API --- From c208d8fc2e43b811ea7ffb8068ff8298bb1230b0 Mon Sep 17 00:00:00 2001 From: dansheps Date: Tue, 5 Mar 2019 15:42:47 -0600 Subject: [PATCH 017/105] * Added CSS to: * Hide URLs * Hide elements with "noprint" class * Added noprint to: * Header Panel * Search Panel, Tags Panel * Buttons * Various list elements * Related elements --- netbox/circuits/tables.py | 2 +- netbox/dcim/tables.py | 16 +++++----- netbox/extras/tables.py | 2 +- netbox/ipam/tables.py | 8 ++--- netbox/project-static/css/base.css | 17 +++++++++++ netbox/secrets/tables.py | 2 +- netbox/templates/_base.html | 2 +- netbox/templates/circuits/circuit.html | 4 +-- netbox/templates/circuits/circuit_list.html | 4 +-- .../templates/circuits/circuittype_list.html | 2 +- netbox/templates/circuits/provider.html | 6 ++-- netbox/templates/circuits/provider_list.html | 4 +-- netbox/templates/dcim/cable.html | 4 +-- netbox/templates/dcim/cable_list.html | 4 +-- .../dcim/console_connections_list.html | 4 +-- netbox/templates/dcim/device.html | 30 +++++++++---------- netbox/templates/dcim/device_inventory.html | 2 +- netbox/templates/dcim/device_list.html | 4 +-- netbox/templates/dcim/devicerole_list.html | 2 +- netbox/templates/dcim/devicetype.html | 4 +-- netbox/templates/dcim/devicetype_list.html | 4 +-- netbox/templates/dcim/inc/consoleport.html | 2 +- .../templates/dcim/inc/consoleserverport.html | 2 +- netbox/templates/dcim/inc/devicebay.html | 2 +- .../dcim/inc/devicetype_component_table.html | 2 +- netbox/templates/dcim/inc/frontport.html | 2 +- netbox/templates/dcim/inc/interface.html | 4 +-- netbox/templates/dcim/inc/inventoryitem.html | 2 +- netbox/templates/dcim/inc/poweroutlet.html | 2 +- netbox/templates/dcim/inc/powerport.html | 2 +- netbox/templates/dcim/inc/rearport.html | 2 +- netbox/templates/dcim/interface.html | 4 +-- .../dcim/interface_connections_list.html | 4 +-- netbox/templates/dcim/inventoryitem_list.html | 4 +-- netbox/templates/dcim/manufacturer_list.html | 2 +- netbox/templates/dcim/platform_list.html | 2 +- .../dcim/power_connections_list.html | 4 +-- netbox/templates/dcim/rack.html | 12 ++++---- .../templates/dcim/rack_elevation_list.html | 4 +-- netbox/templates/dcim/rack_list.html | 4 +-- netbox/templates/dcim/rackgroup_list.html | 4 +-- .../templates/dcim/rackreservation_list.html | 2 +- netbox/templates/dcim/rackrole_list.html | 2 +- netbox/templates/dcim/region_list.html | 4 +-- netbox/templates/dcim/site.html | 12 ++++---- netbox/templates/dcim/site_list.html | 4 +-- .../templates/dcim/virtualchassis_list.html | 4 +-- netbox/templates/extras/configcontext.html | 4 +-- .../templates/extras/configcontext_list.html | 4 +-- netbox/templates/extras/objectchange.html | 4 +-- .../templates/extras/objectchange_list.html | 4 +-- netbox/templates/extras/report.html | 4 +-- netbox/templates/inc/image_attachments.html | 2 +- netbox/templates/inc/search_panel.html | 2 +- netbox/templates/ipam/aggregate.html | 4 +-- netbox/templates/ipam/aggregate_list.html | 4 +-- netbox/templates/ipam/inc/service.html | 2 +- netbox/templates/ipam/ipaddress.html | 6 ++-- netbox/templates/ipam/ipaddress_list.html | 4 +-- netbox/templates/ipam/prefix.html | 4 +-- netbox/templates/ipam/prefix_list.html | 4 +-- netbox/templates/ipam/rir_list.html | 4 +-- netbox/templates/ipam/role_list.html | 2 +- netbox/templates/ipam/service.html | 2 +- netbox/templates/ipam/service_list.html | 4 +-- netbox/templates/ipam/vlan.html | 6 ++-- netbox/templates/ipam/vlan_list.html | 4 +-- netbox/templates/ipam/vlangroup_list.html | 4 +-- netbox/templates/ipam/vlangroup_vlans.html | 2 +- netbox/templates/ipam/vrf.html | 4 +-- netbox/templates/ipam/vrf_list.html | 4 +-- .../secrets/inc/private_key_modal.html | 2 +- netbox/templates/secrets/inc/secret_tr.html | 2 +- netbox/templates/secrets/secret.html | 6 ++-- netbox/templates/secrets/secret_list.html | 4 +-- netbox/templates/secrets/secretrole_list.html | 2 +- netbox/templates/tenancy/tenant.html | 4 +-- netbox/templates/tenancy/tenant_list.html | 4 +-- .../templates/tenancy/tenantgroup_list.html | 2 +- netbox/templates/users/api_tokens.html | 2 +- netbox/templates/users/userkey.html | 4 +-- netbox/templates/utilities/obj_table.html | 4 +-- netbox/templates/virtualization/cluster.html | 6 ++-- .../virtualization/cluster_add_devices.html | 2 +- .../virtualization/cluster_list.html | 4 +-- .../virtualization/clustergroup_list.html | 2 +- .../virtualization/clustertype_list.html | 2 +- .../virtualization/virtualmachine.html | 8 ++--- .../virtualization/virtualmachine_list.html | 4 +-- netbox/tenancy/tables.py | 2 +- netbox/virtualization/tables.py | 4 +-- 91 files changed, 198 insertions(+), 181 deletions(-) diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index c6a215db8..1cddeffb2 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -59,7 +59,7 @@ class CircuitTypeTable(BaseTable): name = tables.LinkColumn() circuit_count = tables.Column(verbose_name='Circuits') actions = tables.TemplateColumn( - template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' + template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) class Meta(BaseTable.Meta): diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 5649c10ef..436b9053d 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -196,7 +196,7 @@ class RegionTable(BaseTable): slug = tables.Column(verbose_name='Slug') actions = tables.TemplateColumn( template_code=REGION_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) @@ -239,7 +239,7 @@ class RackGroupTable(BaseTable): slug = tables.Column() actions = tables.TemplateColumn( template_code=RACKGROUP_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) @@ -258,7 +258,7 @@ class RackRoleTable(BaseTable): rack_count = tables.Column(verbose_name='Racks') color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color') slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, + actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='') class Meta(BaseTable.Meta): @@ -309,7 +309,7 @@ class RackReservationTable(BaseTable): rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) unit_list = tables.Column(orderable=False, verbose_name='Units') actions = tables.TemplateColumn( - template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' + template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) class Meta(BaseTable.Meta): @@ -327,7 +327,7 @@ class ManufacturerTable(BaseTable): 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'}}, + actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='') class Meta(BaseTable.Meta): @@ -463,7 +463,7 @@ class DeviceRoleTable(BaseTable): slug = tables.Column(verbose_name='Slug') actions = tables.TemplateColumn( template_code=DEVICEROLE_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) @@ -492,7 +492,7 @@ class PlatformTable(BaseTable): ) actions = tables.TemplateColumn( template_code=PLATFORM_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) @@ -779,7 +779,7 @@ class VirtualChassisTable(BaseTable): member_count = tables.Column(verbose_name='Members') actions = tables.TemplateColumn( template_code=VIRTUALCHASSIS_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index 5fab8910f..f6933bf48 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -68,7 +68,7 @@ class TagTable(BaseTable): ) actions = tables.TemplateColumn( template_code=TAG_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 026cbc980..3d46452b2 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -203,7 +203,7 @@ class RIRTable(BaseTable): name = tables.LinkColumn(verbose_name='Name') is_private = BooleanColumn(verbose_name='Private') aggregate_count = tables.Column(verbose_name='Aggregates') - actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='') + actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='') class Meta(BaseTable.Meta): model = RIR @@ -288,7 +288,7 @@ class RoleTable(BaseTable): orderable=False, verbose_name='VLANs' ) - actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='') + actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='') class Meta(BaseTable.Meta): model = Role @@ -392,7 +392,7 @@ class VLANGroupTable(BaseTable): site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') vlan_count = tables.Column(verbose_name='VLANs') slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}}, + actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='') class Meta(BaseTable.Meta): @@ -437,7 +437,7 @@ class VLANMemberTable(BaseTable): ) actions = tables.TemplateColumn( template_code=VLAN_MEMBER_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index ad618b5d1..0b18251fd 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -49,6 +49,23 @@ footer p { } } +.noprint { + +} + +@media print { + body { + padding-top: 0px; + } + .noprint { + display: none !important; + } + + a[href]:after { + content: none !important; + } +} + /* Collapse the nav menu on displays less than 960px wide */ @media (max-width: 959px) { .navbar-header { diff --git a/netbox/secrets/tables.py b/netbox/secrets/tables.py index 39d260a6d..a547ef4f8 100644 --- a/netbox/secrets/tables.py +++ b/netbox/secrets/tables.py @@ -23,7 +23,7 @@ class SecretRoleTable(BaseTable): secret_count = tables.Column(verbose_name='Secrets') slug = tables.Column(verbose_name='Slug') actions = tables.TemplateColumn( - template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' + template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) class Meta(BaseTable.Meta): diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 9101e08f7..02b6bb32c 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -54,7 +54,7 @@

{% now 'Y-m-d H:i:s T' %}

-
+

Docs · API · diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index edbab3ed4..890b2a880 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -4,7 +4,7 @@ {% block title %}{{ circuit }}{% endblock %} {% block header %} -

+
-
+
{% if perms.circuits.change_circuit %} diff --git a/netbox/templates/circuits/circuit_list.html b/netbox/templates/circuits/circuit_list.html index 81e09c32b..d686bdf7a 100644 --- a/netbox/templates/circuits/circuit_list.html +++ b/netbox/templates/circuits/circuit_list.html @@ -2,7 +2,7 @@ {% load buttons %} {% block content %} -
+
{% if perms.circuits.add_circuit %} {% add_button 'circuits:circuit_add' %} {% import_button 'circuits:circuit_import' %} @@ -14,7 +14,7 @@
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}
-
+
{% include 'inc/search_panel.html' %} {% include 'inc/tags_panel.html' %}
diff --git a/netbox/templates/circuits/circuittype_list.html b/netbox/templates/circuits/circuittype_list.html index 2b9469042..654d4ab09 100644 --- a/netbox/templates/circuits/circuittype_list.html +++ b/netbox/templates/circuits/circuittype_list.html @@ -2,7 +2,7 @@ {% load buttons %} {% block content %} -
+
{% if perms.circuits.add_circuittype %} {% add_button 'circuits:circuittype_add' %} {% import_button 'circuits:circuittype_import' %} diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index a31f093c9..3dd5d973f 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -5,7 +5,7 @@ {% block title %}{{ provider }}{% endblock %} {% block header %} -
+ -
+
{% if show_graphs %} @@ -521,7 +521,7 @@ {% endfor %} -