Просмотр исходного кода

Fixes #2781: Fixes filter by regions on site and device list

* Add Device filter
dansheps 7 лет назад
Родитель
Сommit
b4d7f9ea43

+ 2 - 24
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(),

+ 5 - 26
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',

+ 2 - 2
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')
 

+ 6 - 105
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:

+ 14 - 98
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',

+ 5 - 5
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')

+ 5 - 93
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)',

+ 14 - 98
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,

+ 6 - 6
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')

+ 25 - 0
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)',
+    )

+ 28 - 0
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

+ 2 - 24
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)',

+ 5 - 26
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',

+ 2 - 2
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