diff --git a/CHANGELOG.md b/CHANGELOG.md index 262c38c1a..a5ab7badb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Enhancements +* [#2813](https://github.com/digitalocean/netbox/issues/2813) - Add tenant group filters * [#3150](https://github.com/digitalocean/netbox/issues/3150) - Fix formatting of cable length during cable trace * [#3085](https://github.com/digitalocean/netbox/issues/3085) - Catch all exceptions during export template rendering diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index e79b78e7b..4decb7166 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -3,13 +3,13 @@ from django.db.models import Q from dcim.models import Site from extras.filters import CustomFieldFilterSet -from tenancy.filterset import TenancyFilterSet +from tenancy.filtersets import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from .constants import CIRCUIT_STATUS_CHOICES from .models import Provider, Circuit, CircuitTermination, CircuitType -class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet): +class ProviderFilter(CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -54,7 +54,7 @@ class CircuitTypeFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, django_filters.FilterSet): +class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index f90cb9503..8e5fbf9f7 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -268,8 +268,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Circuit - # Order the form fields, fields not listed are appended - field_order = ['q', 'type', 'provider', 'status'] + field_order = ['q', 'type', 'provider', 'status', 'site', 'tenant_group', 'tenant', 'commit_rate'] q = forms.CharField( required=False, label='Search' diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 47f052f70..f9f0a57d6 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,13 +1,13 @@ import django_filters from django.contrib.auth.models import User -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from netaddr import EUI from netaddr.core import AddrFormatError from extras.filters import CustomFieldFilterSet -from tenancy.filterset import TenancyFilterSet +from tenancy.filtersets import TenancyFilterSet +from tenancy.models import Tenant from utilities.constants import COLOR_CHOICES from utilities.filters import ( NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter @@ -39,7 +39,7 @@ class RegionFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): +class SiteFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -114,7 +114,7 @@ class RackRoleFilter(NameSlugSearchFilterSet): fields = ['name', 'slug', 'color'] -class RackFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): +class RackFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -180,7 +180,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSe ) -class RackReservationFilter(TenancyFilterSet, django_filters.FilterSet): +class RackReservationFilter(TenancyFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -875,7 +875,7 @@ class InventoryItemFilter(DeviceComponentFilterSet): return queryset.filter(qs_filter) -class VirtualChassisFilter(TenancyFilterSet, django_filters.FilterSet): +class VirtualChassisFilter(django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -891,6 +891,17 @@ class VirtualChassisFilter(TenancyFilterSet, django_filters.FilterSet): to_field_name='slug', label='Site name (slug)', ) + tenant_id = django_filters.ModelMultipleChoiceFilter( + field_name='master__tenant', + queryset=Tenant.objects.all(), + label='Tenant (ID)', + ) + tenant = django_filters.ModelMultipleChoiceFilter( + field_name='master__tenant__slug', + queryset=Tenant.objects.all(), + to_field_name='slug', + label='Tenant (slug)', + ) tag = TagFilter() class Meta: diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 56a1cb9b4..369ebeb23 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -14,7 +14,7 @@ from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEdit from ipam.models import IPAddress, VLAN, VLANGroup from tenancy.forms import TenancyForm from tenancy.formset import TenancyFilterForm -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, @@ -259,8 +259,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Site - # Order the form fields, fields not listed are appended - field_order = ['q', 'status', 'region'] + field_order = ['q', 'status', 'region', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' @@ -591,8 +590,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Rack - # Order the form fields, fields not listed are appended - field_order = ['q', 'site', 'group_id'] + field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' @@ -674,32 +672,6 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm): return unit_choices -class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, forms.Form): - # Order the form fields, fields not listed are appended - field_order = ['q', 'site', 'group_id'] - q = forms.CharField( - required=False, - label='Search' - ) - site = FilterChoiceField( - queryset=Site.objects.all(), - to_field_name='slug', - widget=APISelectMultiple( - api_url="/api/dcim/sites/", - value_field="slug", - ) - ) - group_id = FilterChoiceField( - queryset=RackGroup.objects.select_related('site'), - label='Rack group', - null_label='-- None --', - widget=APISelectMultiple( - api_url="/api/dcim/rack-groups/", - null_option=True, - ) - ) - - class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RackReservation.objects.all(), @@ -728,6 +700,31 @@ class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): nullable_fields = [] +class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm): + field_order = ['q', 'site', 'group_id', 'tenant_group', 'tenant'] + q = forms.CharField( + required=False, + label='Search' + ) + site = FilterChoiceField( + queryset=Site.objects.all(), + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field="slug", + ) + ) + group_id = FilterChoiceField( + queryset=RackGroup.objects.select_related('site'), + label='Rack group', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/dcim/rack-groups/", + null_option=True, + ) + ) + + # # Manufacturers # @@ -1622,8 +1619,10 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF class DeviceFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Device - # Order the form fields, fields not listed are appended - field_order = ['q', 'region', 'site', 'rack_group_id', 'rack_id', 'role'] + field_order = [ + 'q', 'region', 'site', 'rack_group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant', + 'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip', + ] q = forms.CharField( required=False, label='Search' @@ -3074,9 +3073,31 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field="slug", + ) + ) + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + null_option=True, + filter_for={ + 'tenant': 'group' + } + ) ) tenant = FilterChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', null_label='-- None --', + widget=APISelectMultiple( + api_url="/api/tenancy/tenants/", + value_field="slug", + null_option=True, + ) ) diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index 712dfd6d8..d5457a5a6 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -4,7 +4,7 @@ from django.db.models import Q from taggit.models import Tag from dcim.models import DeviceRole, Platform, Region, Site -from tenancy.filterset import TenancyFilterSet +from tenancy.models import Tenant, TenantGroup from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap @@ -122,7 +122,7 @@ class TopologyMapFilter(django_filters.FilterSet): fields = ['name', 'slug'] -class ConfigContextFilter(TenancyFilterSet, django_filters.FilterSet): +class ConfigContextFilter(django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -171,6 +171,28 @@ class ConfigContextFilter(TenancyFilterSet, django_filters.FilterSet): to_field_name='slug', label='Platform (slug)', ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant_groups', + queryset=TenantGroup.objects.all(), + label='Tenant group', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant_groups__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant group (slug)', + ) + tenant_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenants', + queryset=Tenant.objects.all(), + label='Tenant', + ) + tenant = django_filters.ModelMultipleChoiceFilter( + field_name='tenants__slug', + queryset=Tenant.objects.all(), + to_field_name='slug', + label='Tenant (slug)', + ) class Meta: model = ConfigContext diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index a956597d3..54eee0c5c 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -8,7 +8,7 @@ from taggit.forms import TagField from taggit.models import Tag from dcim.models import DeviceRole, Platform, Region, Site -from tenancy.formset import TenancyFilterForm +from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, @@ -274,7 +274,7 @@ class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm): ] -class ConfigContextFilterForm(TenancyFilterForm, BootstrapMixin, forms.Form): +class ConfigContextFilterForm(BootstrapMixin, forms.Form): q = forms.CharField( required=False, label='Search' @@ -311,10 +311,22 @@ class ConfigContextFilterForm(TenancyFilterForm, BootstrapMixin, forms.Form): value_field="slug", ) ) - - class Meta: - # Order the form fields, fields not listed are appended - field_order = ['q', 'type', 'provider', 'status'] + tenant_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/tenancy/tenant-groups/", + value_field="slug", + ) + ) + tenant = FilterChoiceField( + queryset=Tenant.objects.all(), + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/tenancy/tenants/", + value_field="slug", + ) + ) # diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 3050a7901..a6e986117 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -6,14 +6,14 @@ from netaddr.core import AddrFormatError from dcim.models import Site, Device, Interface from extras.filters import CustomFieldFilterSet -from tenancy.filterset import TenancyFilterSet +from tenancy.filtersets import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from virtualization.models import VirtualMachine from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF -class VRFFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): +class VRFFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -49,7 +49,7 @@ class RIRFilter(NameSlugSearchFilterSet): fields = ['name', 'slug', 'is_private'] -class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet): +class AggregateFilter(CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -97,7 +97,7 @@ class RoleFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): +class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -234,7 +234,7 @@ class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.Filter return queryset.filter(prefix__net_mask_length=value) -class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): +class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -364,7 +364,7 @@ class VLANGroupFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class VLANFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSet): +class VLANFilter(TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 1bc83a9d0..a8e9df6c5 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -100,8 +100,7 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VRF - # Order the form fields, fields not listed are appended - field_order = ['q'] + field_order = ['q', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' @@ -492,8 +491,10 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Prefix - # Order the form fields, fields not listed are appended - field_order = ['q', 'within_include', 'family', 'mask_length', 'vrf'] + field_order = [ + 'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'site', 'role', 'tenant_group', 'tenant', + 'is_pool', 'expand', + ] q = forms.CharField( required=False, label='Search' @@ -931,8 +932,9 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form): class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = IPAddress - # Order the form fields, fields not listed are appended - field_order = ['q', 'parent', 'family', 'mask_length', 'vrf'] + field_order = [ + 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'tenant_group', 'tenant', + ] q = forms.CharField( required=False, label='Search' @@ -1200,8 +1202,7 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VLAN - # Order the form fields, fields not listed are appended - field_order = ['q', 'site', 'group_id'] + field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' diff --git a/netbox/tenancy/filterset.py b/netbox/tenancy/filtersets.py similarity index 100% rename from netbox/tenancy/filterset.py rename to netbox/tenancy/filtersets.py diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index e7e065ff7..636cfed03 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -118,6 +118,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): # # Tenancy form extension # + class TenancyForm(ChainedFieldsMixin, forms.Form): tenant_group = forms.ModelChoiceField( queryset=TenantGroup.objects.all(), diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index a3ae66f3d..e71693ac6 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -1,12 +1,11 @@ import django_filters -from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from netaddr import EUI from netaddr.core import AddrFormatError from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet -from tenancy.filterset import TenancyFilterSet +from tenancy.filtersets import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 2e881b184..e61c0e320 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -9,7 +9,7 @@ from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomField from ipam.models import IPAddress from tenancy.forms import TenancyForm from tenancy.formset import TenancyFilterForm -from tenancy.models import Tenant, TenantGroup +from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm, @@ -337,8 +337,8 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): class Meta: model = VirtualMachine fields = [ - 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', - 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data', + 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', + 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data', ] help_texts = { 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered " @@ -523,8 +523,10 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VirtualMachine - # Order the form fields, fields not listed are appended - field_order = ['q', 'cluster_group', 'cluster_type', 'cluster_id', 'region', 'site'] + field_order = [ + 'q', 'cluster_group', 'cluster_type', 'cluster_id', 'status', 'role', 'region', 'site', 'tenant_group', + 'tenant', 'platform', + ] q = forms.CharField( required=False, label='Search'