diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index c6e3934e7..2bf55d857 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -1,3 +1,12 @@ +# v2.6.11 (2020-01-03) + +## Bug Fixes + +* [#3831](https://github.com/netbox-community/netbox/issues/3831) - Fix API-driven filter field rendering (#3812 regression) +* [#3833](https://github.com/netbox-community/netbox/issues/3833) - Add missing region filters for multiple objects + +--- + # v2.6.10 (2020-01-02) ## Enhancements diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 3f3f974b7..0fff6fda0 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -18,6 +18,17 @@ class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='circuits__terminations__site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='circuits__terminations__site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( field_name='circuits__terminations__site', queryset=Site.objects.all(), diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index afd6f0abb..d5d78e7bd 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -104,6 +104,18 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', @@ -302,6 +314,9 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm widget=APISelectMultiple( api_url="/api/dcim/regions/", value_field="slug", + filter_for={ + 'site': 'region' + } ) ) site = FilterChoiceField( diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 3e7fcf3a1..969a61d01 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -94,6 +94,17 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet class RackGroupFilter(NameSlugSearchFilterSet): + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -126,6 +137,17 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -889,6 +911,28 @@ class InventoryItemFilter(DeviceComponentFilterSet): method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='device__site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='device__site__region__in', + to_field_name='slug', + label='Region (slug)', + ) + site_id = django_filters.ModelMultipleChoiceFilter( + field_name='device__site', + queryset=Site.objects.all(), + label='Site (ID)', + ) + site = django_filters.ModelMultipleChoiceFilter( + field_name='device__site__slug', + queryset=Site.objects.all(), + to_field_name='slug', + label='Site name (slug)', + ) device_id = django_filters.ModelChoiceFilter( queryset=Device.objects.all(), label='Device (ID)', @@ -938,6 +982,17 @@ class VirtualChassisFilter(django_filters.FilterSet): method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='master__site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='master__site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( field_name='master__site', queryset=Site.objects.all(), @@ -1136,6 +1191,17 @@ class PowerPanelFilter(django_filters.FilterSet): method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -1174,6 +1240,17 @@ class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='power_panel__site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='power_panel__site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( field_name='power_panel__site', queryset=Site.objects.all(), diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 3141e7737..019511eaa 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -392,6 +392,18 @@ class RackGroupCSVForm(forms.ModelForm): class RackGroupFilterForm(BootstrapMixin, forms.Form): + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', @@ -660,11 +672,23 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Rack - field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] + field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', @@ -676,16 +700,15 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): } ) ) - group_id = ChainedModelChoiceField( - label='Rack group', - queryset=RackGroup.objects.prefetch_related('site'), - chains=( - ('site', 'site'), + group_id = FilterChoiceField( + queryset=RackGroup.objects.prefetch_related( + 'site' ), - required=False, + label='Rack group', + null_label='-- None --', widget=APISelectMultiple( api_url="/api/dcim/rack-groups/", - null_option=True, + null_option=True ) ) status = forms.MultipleChoiceField( @@ -3879,6 +3902,29 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): required=False, label='Search' ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) + site = FilterChoiceField( + queryset=Site.objects.all(), + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field="slug", + filter_for={ + 'device_id': 'site' + } + ) + ) device_id = FilterChoiceField( queryset=Device.objects.all(), required=False, @@ -4044,6 +4090,18 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', @@ -4149,6 +4207,18 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', @@ -4369,6 +4439,18 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index c0695aeaf..46d2a45fd 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -4,10 +4,10 @@ from django.core.exceptions import ValidationError from django.db.models import Q from netaddr.core import AddrFormatError -from dcim.models import Site, Device, Interface +from dcim.models import Device, Interface, Region, Site from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from tenancy.filtersets import TenancyFilterSet -from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter +from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from virtualization.models import VirtualMachine from .choices import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF @@ -149,6 +149,17 @@ class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterS to_field_name='rd', label='VRF (RD)', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -375,6 +386,17 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt class VLANGroupFilter(NameSlugSearchFilterSet): + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -400,6 +422,17 @@ class VLANFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 4b0075d9a..3f6e5f53b 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -3,7 +3,7 @@ from django.core.exceptions import MultipleObjectsReturned from django.core.validators import MaxValueValidator, MinValueValidator from taggit.forms import TagField -from dcim.models import Site, Rack, Device, Interface +from dcim.models import Device, Interface, Rack, Region, Site from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant @@ -492,8 +492,8 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Prefix field_order = [ - 'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'site', 'role', 'tenant_group', 'tenant', - 'is_pool', 'expand', + 'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'region', 'site', 'role', 'tenant_group', + 'tenant', 'is_pool', 'expand', ] q = forms.CharField( required=False, @@ -534,6 +534,18 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) required=False, widget=StaticSelect2Multiple() ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', @@ -1034,6 +1046,18 @@ class VLANGroupCSVForm(forms.ModelForm): class VLANGroupFilterForm(BootstrapMixin, forms.Form): + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region', + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', @@ -1215,11 +1239,24 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VLAN - field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] + field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region', + 'group_id': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', diff --git a/netbox/utilities/templates/widgets/select_api.html b/netbox/utilities/templates/widgets/select_api.html index 0146a31ff..d9516086b 100644 --- a/netbox/utilities/templates/widgets/select_api.html +++ b/netbox/utilities/templates/widgets/select_api.html @@ -1,5 +1,9 @@ - +{% for group_name, group_choices, group_index in widget.optgroups %} + {% if group_name %}{% endif %} + {% for option in group_choices %} + {% if option.attrs.selected or option.value == "null" %}{% include option.template_name with widget=option %}{% endif %} + {% endfor %} + {% if group_name %}{% endif %} +{% endfor %} diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index a67296353..8b492c865 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -37,6 +37,27 @@ class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', + label='Region (slug)', + ) + site_id = django_filters.ModelMultipleChoiceFilter( + queryset=Site.objects.all(), + label='Site (ID)', + ) + site = django_filters.ModelMultipleChoiceFilter( + field_name='site__slug', + queryset=Site.objects.all(), + to_field_name='slug', + label='Site (slug)', + ) group_id = django_filters.ModelMultipleChoiceFilter( queryset=ClusterGroup.objects.all(), label='Parent group (ID)', @@ -61,16 +82,6 @@ class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): queryset=Tenant.objects.all(), label="Tenant (ID)" ) - site_id = django_filters.ModelMultipleChoiceFilter( - queryset=Site.objects.all(), - label='Site (ID)', - ) - site = django_filters.ModelMultipleChoiceFilter( - field_name='site__slug', - queryset=Site.objects.all(), - to_field_name='slug', - label='Site (slug)', - ) tag = TagFilter() class Meta: diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index acf79a2b7..2d454494a 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -186,6 +186,29 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Cluster q = forms.CharField(required=False, label='Search') + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) + site = FilterChoiceField( + queryset=Site.objects.all(), + to_field_name='slug', + null_label='-- None --', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field='slug', + null_option=True, + ) + ) type = FilterChoiceField( queryset=ClusterType.objects.all(), to_field_name='slug', @@ -215,17 +238,6 @@ class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - site = FilterChoiceField( - queryset=Site.objects.all(), - to_field_name='slug', - null_label='-- None --', - required=False, - widget=APISelectMultiple( - api_url="/api/dcim/sites/", - value_field='slug', - null_option=True, - ) - ) class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): @@ -585,7 +597,9 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil widget=APISelectMultiple( api_url='/api/dcim/regions/', value_field="slug", - null_option=True, + filter_for={ + 'site': 'region' + } ) ) site = FilterChoiceField(